using System.Globalization;
using System.Security.Cryptography;
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.Options;
using Npgsql;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Observability;

namespace Nexus.Api.Services;

/// <summary>
/// Durable Nexus approval workflow for OpenClaw-owned agents. This service
/// deliberately does not retry agents.create: an uncertain dispatch can only
/// transition through a read-only reconciliation attempt requested by an
/// owner.
/// </summary>
public sealed partial class AgentProposalService(
    NexusDbContext db,
    IGatewayConnector connector,
    IOpenClawAgentConfigurationService agentConfiguration,
    IOpenClawWriteGate writeGate,
    IOptions<AgentProvisioningOptions> provisioningOptions,
    AgentProvisioningSignal signal,
    ILogger<AgentProposalService> logger) : IAgentProposalService
{
    private const int MaxNameLength = 80;
    private const int MaxRoleLength = 160;
    private const int MaxDescriptionLength = 4000;
    private const int MaxModelLength = 240;
    private const int MaxIdempotencyKeyLength = 200;

    private static readonly string[] StandardFileNames =
    [
        "AGENTS.md",
        "SOUL.md",
        "TOOLS.md",
        "IDENTITY.md",
        "USER.md",
        "HEARTBEAT.md",
        "BOOTSTRAP.md",
        "MEMORY.md"
    ];

    private static readonly IReadOnlyDictionary<string, string> CanonicalFileNames =
        StandardFileNames.ToDictionary(
            name => name,
            name => name,
            StringComparer.OrdinalIgnoreCase);

    private static readonly JsonSerializerOptions InternalJsonOptions = new(
        JsonSerializerDefaults.Web);

    public async Task<AgentCreateOptionsDto> GetCreateOptionsAsync(
        CancellationToken cancellationToken = default)
    {
        var gate = await EvaluateGateAsync(cancellationToken);
        var agentInventory = await TryReadAgentInventoryAsync(cancellationToken);
        var agents = agentInventory.Items;
        var models = await TryListModelsAsync(cancellationToken);

        return new AgentCreateOptionsDto(
            CanSubmitProposal: true,
            CanProvision: gate.Ok,
            State: gate.State,
            Reason: gate.Message,
            WorkspaceRoot: NormalizeWorkspaceRoot(provisioningOptions.Value.WorkspaceRoot)
                ?? "not_configured",
            ExistingAgentIds: agents
                .Select(item => item.AgentId)
                .Order(StringComparer.Ordinal)
                .ToArray(),
            Models: models,
            StandardFiles: StandardFileNames,
            CheckedAt: DateTimeOffset.UtcNow);
    }

    public async Task<AgentProposalCollectionDto> GetAsync(
        int limit = 50,
        string? cursor = null,
        string? status = null,
        CancellationToken cancellationToken = default)
    {
        var boundedLimit = Math.Clamp(limit, 1, 200);
        var query = db.AgentProposals.AsNoTracking();
        if (!string.IsNullOrWhiteSpace(status))
            query = query.Where(item => item.Status == status.Trim().ToLowerInvariant());

        if (!string.IsNullOrWhiteSpace(cursor))
        {
            if (!TryDecodeCursor(cursor, out var beforeCreatedAt, out var beforeId))
            {
                throw new AgentProposalValidationException(
                    "cursor",
                    "The proposal cursor is invalid.");
            }

            query = beforeId is null
                ? query.Where(item => item.CreatedAt < beforeCreatedAt)
                : query.Where(item =>
                    item.CreatedAt < beforeCreatedAt
                    || (item.CreatedAt == beforeCreatedAt
                        && item.Id.CompareTo(beforeId.Value) < 0));
        }

        var rows = await query
            .OrderByDescending(item => item.CreatedAt)
            .ThenByDescending(item => item.Id)
            .Take(boundedLimit + 1)
            .ToListAsync(cancellationToken);
        var hasMore = rows.Count > boundedLimit;
        var selected = rows.Take(boundedLimit).ToArray();

        return new AgentProposalCollectionDto(
            selected.Select(item => Map(item, includeFileContent: false)).ToArray(),
            hasMore && selected.Length > 0
                ? EncodeCursor(selected[^1].CreatedAt, selected[^1].Id)
                : null,
            DateTimeOffset.UtcNow);
    }

    public async Task<AgentProposalDto?> GetByIdAsync(
        Guid id,
        bool includeFileContent = true,
        CancellationToken cancellationToken = default)
    {
        var proposal = await db.AgentProposals
            .AsNoTracking()
            .SingleOrDefaultAsync(item => item.Id == id, cancellationToken);
        return proposal is null ? null : Map(proposal, includeFileContent);
    }

    public async Task<AgentProposalOperationDto> CreateAsync(
        CreateAgentProposalRequest request,
        string source,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(request);
        ValidateInvocation(invocation);
        var normalizedSource = NormalizeSource(source);
        var normalized = NormalizeProposal(request);
        var idempotencyKey =
            normalizedSource == "manual"
                || string.IsNullOrWhiteSpace(request.ClientRequestId)
                ? invocation.IdempotencyKey
                : request.ClientRequestId.Trim();
        ValidateIdempotencyKey(idempotencyKey);

        var requestHash = Hash(JsonSerializer.Serialize(
            new
            {
                Source = normalizedSource,
                normalized.Name,
                normalized.AgentId,
                normalized.Role,
                normalized.Description,
                normalized.Model,
                normalized.Emoji,
                normalized.Avatar,
                normalized.Workspace,
                normalized.FilesHash
            },
            InternalJsonOptions));
        var keyHash = Hash(idempotencyKey);
        var replay = await ReplayAsync(
            "agent-proposal.create",
            keyHash,
            requestHash,
            invocation.CorrelationId,
            cancellationToken);
        if (replay is not null)
            return replay;

        var now = DateTimeOffset.UtcNow;
        var proposal = new AgentProposal
        {
            Source = normalizedSource,
            RequestedName = normalized.Name,
            RequestedAgentId = normalized.AgentId,
            Role = normalized.Role,
            Description = normalized.Description,
            Model = normalized.Model,
            Emoji = normalized.Emoji,
            Avatar = normalized.Avatar,
            Workspace = normalized.Workspace,
            StandardFilesJson = normalized.FilesJson,
            StandardFilesHash = normalized.FilesHash,
            Status = AgentProposalStates.AwaitingApproval,
            RequestedBy = NormalizeActor(invocation.Actor),
            CreatedAt = now,
            UpdatedAt = now
        };
        var claim = Claim(
            "agent-proposal.create",
            keyHash,
            requestHash,
            proposal.Id,
            "completed",
            AgentProposalStates.AwaitingApproval);
        db.AgentProposals.Add(proposal);
        db.OperationClaims.Add(claim);
        db.OutboxEvents.Add(Event(
            "agent.proposal.created",
            proposal,
            new { proposal.Id, proposal.Source, proposal.Status }));

        try
        {
            await db.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateException)
        {
            db.ChangeTracker.Clear();
            var concurrentReplay = await ReplayAsync(
                "agent-proposal.create",
                keyHash,
                requestHash,
                invocation.CorrelationId,
                cancellationToken);
            if (concurrentReplay is not null)
                return concurrentReplay;
            throw;
        }

        return Result(
            true,
            AgentProposalStates.AwaitingApproval,
            "Agent proposal recorded. OpenClaw has not been mutated.",
            proposal,
            invocation.CorrelationId);
    }

    public Task<AgentProposalOperationDto> ApproveAsync(
        Guid id,
        AgentProposalActionRequest request,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
        => MutateApprovalAsync(
            id,
            request,
            invocation,
            "agent-proposal.approve",
            cancellationToken,
            approve: true);

    public Task<AgentProposalOperationDto> RejectAsync(
        Guid id,
        AgentProposalActionRequest request,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
        => MutateApprovalAsync(
            id,
            request,
            invocation,
            "agent-proposal.reject",
            cancellationToken,
            approve: false);

    public async Task<AgentProposalOperationDto> RetryAsync(
        Guid id,
        AgentProposalActionRequest request,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
    {
        ValidateInvocation(invocation);
        ValidateReason(request.Reason);
        var requestHash = Hash($"{id:N}\n{request.ExpectedRevision}\n{request.Reason?.Trim()}");
        var keyHash = Hash(invocation.IdempotencyKey);
        var replay = await ReplayAsync(
            "agent-proposal.retry",
            keyHash,
            requestHash,
            invocation.CorrelationId,
            cancellationToken);
        if (replay is not null)
            return replay;

        var proposal = await db.AgentProposals
            .Include(item => item.ProvisionRequests)
            .SingleOrDefaultAsync(item => item.Id == id, cancellationToken);
        if (proposal is null)
        {
            return await RecordFailureClaimAsync(
                "agent-proposal.retry",
                keyHash,
                requestHash,
                id,
                "not_found",
                "Agent proposal was not found.",
                invocation.CorrelationId,
                cancellationToken);
        }

        if (proposal.Revision != request.ExpectedRevision)
        {
            return await RecordFailureClaimAsync(
                "agent-proposal.retry",
                keyHash,
                requestHash,
                id,
                "concurrency_conflict",
                "Agent proposal changed since it was loaded.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                "Reload the proposal and retry with its current revision.");
        }

        if (proposal.Status is not (
                AgentProposalStates.Failed
                or AgentProposalStates.Partial
                or AgentProposalStates.InDoubt))
        {
            return await RecordFailureClaimAsync(
                "agent-proposal.retry",
                keyHash,
                requestHash,
                id,
                "invalid_state",
                $"Proposal state '{proposal.Status}' cannot be retried.",
                invocation.CorrelationId,
                cancellationToken,
                proposal);
        }

        var gate = await EvaluateGateAsync(cancellationToken);
        if (!gate.Ok)
        {
            return await RecordFailureClaimAsync(
                "agent-proposal.retry",
                keyHash,
                requestHash,
                id,
                gate.State,
                gate.Message ?? "Agent provisioning is blocked by the current OpenClaw policy.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                gate.Recovery);
        }

        var stage = proposal.Status == AgentProposalStates.InDoubt
            ? AgentProvisionStages.ReconcileAgent
            : !string.IsNullOrWhiteSpace(proposal.OpenClawAgentId)
                ? AgentProvisionStages.FinalizeFiles
                : AgentProvisionStages.CreateAgent;
        var provision = BuildProvisionRequest(
            proposal,
            stage,
            invocation,
            keyHash);
        proposal.Status = AgentProposalStates.Provisioning;
        proposal.LastErrorCode = null;
        proposal.LastErrorMessage = null;
        Touch(proposal);

        db.AgentProvisionRequests.Add(provision);
        db.OperationClaims.Add(Claim(
            "agent-proposal.retry",
            keyHash,
            requestHash,
            proposal.Id,
            "completed",
            AgentProposalStates.Provisioning));
        db.OutboxEvents.Add(Event(
            "agent.provision.retry_requested",
            proposal,
            new
            {
                ProposalId = proposal.Id,
                ProvisionRequestId = provision.Id,
                provision.Attempt,
                provision.Stage
            }));
        var retryRace = await SaveMutationOrResolveRaceAsync(
            "agent-proposal.retry",
            keyHash,
            requestHash,
            proposal.Id,
            invocation.CorrelationId,
            cancellationToken);
        if (retryRace is not null)
            return retryRace;
        signal.Notify();

        return Result(
            true,
            AgentProposalStates.Provisioning,
            stage == AgentProvisionStages.ReconcileAgent
                ? "A read-only reconciliation was queued. agents.create will not be repeated."
                : "An explicit provisioning retry was queued.",
            proposal,
            invocation.CorrelationId);
    }

    public async Task RecoverInterruptedRequestsAsync(
        CancellationToken cancellationToken = default)
    {
        var interrupted = await db.AgentProvisionRequests
            .Include(item => item.Proposal)
            .Where(item => item.Status == AgentProvisionRequestStates.Dispatching)
            .ToListAsync(cancellationToken);
        if (interrupted.Count == 0)
            return;

        foreach (var request in interrupted)
        {
            var createdAgentKnown =
                !string.IsNullOrWhiteSpace(request.OpenClawAgentId)
                || !string.IsNullOrWhiteSpace(request.Proposal.OpenClawAgentId);
            request.Stage = createdAgentKnown
                ? AgentProvisionStages.FinalizeFiles
                : AgentProvisionStages.ReconcileAgent;
            request.Status = AgentProvisionRequestStates.Queued;
            request.LastErrorCode = "process_interrupted";
            request.LastErrorMessage = createdAgentKnown
                ? "Nexus restarted while agent files were being verified; safe file finalization was queued."
                : "Nexus restarted after the agents.create dispatch boundary; read-only reconciliation was queued.";
            request.LeaseOwner = null;
            request.LeaseUntil = null;
            request.CompletedAt = null;
            Touch(request);

            request.Proposal.Status = createdAgentKnown
                ? AgentProposalStates.Partial
                : AgentProposalStates.InDoubt;
            request.Proposal.LastErrorCode = request.LastErrorCode;
            request.Proposal.LastErrorMessage = request.LastErrorMessage;
            Touch(request.Proposal);
            db.OutboxEvents.Add(Event(
                "agent.provision.interrupted",
                request.Proposal,
                new
                {
                    request.ProposalId,
                    ProvisionRequestId = request.Id,
                    ProvisionRequestStatus = request.Status,
                    ProposalStatus = request.Proposal.Status,
                    request.Stage
                }));
        }

        await db.SaveChangesAsync(cancellationToken);
        signal.Notify();
    }

    public async Task<bool> ProcessNextAsync(
        CancellationToken cancellationToken = default)
    {
        var request = await ClaimNextRequestAsync(cancellationToken);
        if (request is null)
            return false;

        using var activity = NexusTelemetry.ActivitySource.StartActivity(
            "nexus.agent.provision",
            ActivityKind.Internal);
        activity?.SetTag("nexus.provision.stage", request.Stage);
        var stopwatch = Stopwatch.StartNew();
        var outcome = "processed";
        try
        {
            var gate = await EvaluateGateAsync(cancellationToken);
            if (!gate.Ok)
            {
                outcome = "blocked";
                await CompleteFailureAsync(
                    request,
                    AgentProvisionRequestStates.Blocked,
                    AgentProposalStates.Failed,
                    gate.State,
                    gate.Message ?? "Agent provisioning is blocked by the current OpenClaw policy.",
                    cancellationToken);
                return true;
            }

            switch (request.Stage)
            {
                case AgentProvisionStages.ReconcileAgent:
                    await ReconcileOnlyAsync(request, cancellationToken);
                    break;
                case AgentProvisionStages.FinalizeFiles:
                    await FinalizeFilesAsync(request, cancellationToken);
                    break;
                case AgentProvisionStages.CreateAgent:
                    await CreateAgentAsync(request, cancellationToken);
                    break;
                default:
                    outcome = "invalid_stage";
                    await CompleteFailureAsync(
                        request,
                        AgentProvisionRequestStates.Failed,
                        AgentProposalStates.Failed,
                        "invalid_stage",
                        "Provisioning request contains an unsupported stage.",
                        cancellationToken);
                    break;
            }

            return true;
        }
        catch
        {
            outcome = "error";
            throw;
        }
        finally
        {
            stopwatch.Stop();
            activity?.SetTag("nexus.provision.outcome", outcome);
            NexusTelemetry.AgentProvisionDuration.Record(
                stopwatch.Elapsed.TotalMilliseconds,
                new KeyValuePair<string, object?>("stage", request.Stage),
                new KeyValuePair<string, object?>("outcome", outcome));
        }
    }

    private async Task<AgentProvisionRequest?> ClaimNextRequestAsync(
        CancellationToken cancellationToken)
    {
        IDbContextTransaction? transaction = null;
        try
        {
            var now = DateTimeOffset.UtcNow;
            IQueryable<AgentProvisionRequest> query;
            if (db.Database.IsRelational())
            {
                transaction = await db.Database.BeginTransactionAsync(
                    cancellationToken);
                query = db.AgentProvisionRequests.FromSqlInterpolated(
                    $"""
                    SELECT *
                    FROM "AgentProvisionRequests"
                    WHERE "Status" = 'queued'
                       OR (
                           "Status" = 'dispatching'
                           AND "LeaseUntil" IS NOT NULL
                           AND "LeaseUntil" <= {now}
                       )
                    ORDER BY "CreatedAt"
                    LIMIT 1
                    FOR UPDATE SKIP LOCKED
                    """);
            }
            else
            {
                query = db.AgentProvisionRequests
                    .Where(item =>
                        item.Status == AgentProvisionRequestStates.Queued
                        || (
                            item.Status == AgentProvisionRequestStates.Dispatching
                            && item.LeaseUntil != null
                            && item.LeaseUntil <= now))
                    .OrderBy(item => item.CreatedAt);
            }

            var request = await query
                .Include(item => item.Proposal)
                .FirstOrDefaultAsync(cancellationToken);
            if (request is null)
            {
                if (transaction is not null)
                    await transaction.CommitAsync(cancellationToken);
                return null;
            }

            var reclaimed = request.Status ==
                            AgentProvisionRequestStates.Dispatching;
            if (reclaimed)
            {
                var createdAgentKnown =
                    !string.IsNullOrWhiteSpace(request.OpenClawAgentId)
                    || !string.IsNullOrWhiteSpace(
                        request.Proposal.OpenClawAgentId);
                request.Stage = createdAgentKnown
                    ? AgentProvisionStages.FinalizeFiles
                    : AgentProvisionStages.ReconcileAgent;
                request.LastErrorCode = "lease_expired";
                request.LastErrorMessage = createdAgentKnown
                    ? "The previous file-finalization lease expired; safe finalization was reclaimed."
                    : "The previous create dispatch lease expired; read-only reconciliation was reclaimed.";
                request.Proposal.Status = createdAgentKnown
                    ? AgentProposalStates.Partial
                    : AgentProposalStates.InDoubt;
                request.Proposal.LastErrorCode = request.LastErrorCode;
                request.Proposal.LastErrorMessage =
                    request.LastErrorMessage;
                Touch(request.Proposal);
                db.OutboxEvents.Add(Event(
                    "agent.provision.lease_reclaimed",
                    request.Proposal,
                    new
                    {
                        request.ProposalId,
                        ProvisionRequestId = request.Id,
                        request.Stage
                    }));
            }

            request.Status = AgentProvisionRequestStates.Dispatching;
            request.DispatchStartedAt = now;
            request.LeaseOwner = Truncate(Environment.MachineName, 160);
            request.LeaseUntil = now.AddSeconds(Math.Clamp(
                provisioningOptions.Value.LeaseSeconds,
                15,
                300));
            Touch(request);
            await db.SaveChangesAsync(cancellationToken);
            if (transaction is not null)
                await transaction.CommitAsync(cancellationToken);
            return request;
        }
        finally
        {
            if (transaction is not null)
                await transaction.DisposeAsync();
        }
    }

    private async Task<AgentProposalOperationDto> MutateApprovalAsync(
        Guid id,
        AgentProposalActionRequest request,
        OpenClawInvocationMetadata invocation,
        string operation,
        CancellationToken cancellationToken,
        bool approve)
    {
        ValidateInvocation(invocation);
        ValidateReason(request.Reason);
        var requestHash = Hash($"{id:N}\n{request.ExpectedRevision}\n{request.Reason?.Trim()}");
        var keyHash = Hash(invocation.IdempotencyKey);
        var replay = await ReplayAsync(
            operation,
            keyHash,
            requestHash,
            invocation.CorrelationId,
            cancellationToken);
        if (replay is not null)
            return replay;

        var proposal = await db.AgentProposals
            .Include(item => item.ProvisionRequests)
            .SingleOrDefaultAsync(item => item.Id == id, cancellationToken);
        if (proposal is null)
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                "not_found",
                "Agent proposal was not found.",
                invocation.CorrelationId,
                cancellationToken);
        }

        if (proposal.Revision != request.ExpectedRevision)
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                "concurrency_conflict",
                "Agent proposal changed since it was loaded.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                "Reload the proposal and retry with its current revision.");
        }

        if (proposal.Status != AgentProposalStates.AwaitingApproval)
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                "invalid_state",
                $"Proposal state '{proposal.Status}' cannot be {(approve ? "approved" : "rejected")}.",
                invocation.CorrelationId,
                cancellationToken,
                proposal);
        }

        if (!approve)
        {
            proposal.Status = AgentProposalStates.Rejected;
            proposal.RejectedBy = NormalizeActor(invocation.Actor);
            proposal.RejectionReason = NormalizeOptional(request.Reason, 1000);
            proposal.RejectedAt = DateTimeOffset.UtcNow;
            Touch(proposal);
            db.OperationClaims.Add(Claim(
                operation,
                keyHash,
                requestHash,
                proposal.Id,
                "completed",
                AgentProposalStates.Rejected));
            db.OutboxEvents.Add(Event(
                "agent.proposal.rejected",
                proposal,
                new { proposal.Id, proposal.Status }));
            var race = await SaveMutationOrResolveRaceAsync(
                operation,
                keyHash,
                requestHash,
                proposal.Id,
                invocation.CorrelationId,
                cancellationToken);
            if (race is not null)
                return race;
            return Result(
                true,
                AgentProposalStates.Rejected,
                "Agent proposal rejected. OpenClaw was not mutated.",
                proposal,
                invocation.CorrelationId);
        }

        var gate = await EvaluateGateAsync(cancellationToken);
        if (!gate.Ok)
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                gate.State,
                gate.Message ?? "Agent provisioning is blocked by the current OpenClaw policy.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                gate.Recovery);
        }

        var inventory = await TryReadAgentInventoryAsync(cancellationToken);
        if (!inventory.Success)
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                "gateway_unavailable",
                "The live OpenClaw agent inventory could not be verified.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                "Restore agents.list access before approving an agent proposal.");
        }

        var existingAgents = inventory.Items;
        if (existingAgents.Any(agent => string.Equals(
                agent.AgentId,
                proposal.RequestedAgentId,
                StringComparison.Ordinal)))
        {
            return await RecordFailureClaimAsync(
                operation,
                keyHash,
                requestHash,
                id,
                "agent_exists",
                $"OpenClaw already contains agent '{proposal.RequestedAgentId}'.",
                invocation.CorrelationId,
                cancellationToken,
                proposal,
                "Choose a different name or refresh the live agent inventory.");
        }

        proposal.Status = AgentProposalStates.Provisioning;
        proposal.ApprovedBy = NormalizeActor(invocation.Actor);
        proposal.ApprovedAt = DateTimeOffset.UtcNow;
        proposal.LastErrorCode = null;
        proposal.LastErrorMessage = null;
        Touch(proposal);
        var provision = BuildProvisionRequest(
            proposal,
            AgentProvisionStages.CreateAgent,
            invocation,
            keyHash);
        db.AgentProvisionRequests.Add(provision);
        db.OperationClaims.Add(Claim(
            operation,
            keyHash,
            requestHash,
            proposal.Id,
            "completed",
            AgentProposalStates.Provisioning));
        db.OutboxEvents.Add(Event(
            "agent.provision.requested",
            proposal,
            new
            {
                ProposalId = proposal.Id,
                ProvisionRequestId = provision.Id,
                provision.Attempt,
                provision.Stage
            }));
        var approvalRace = await SaveMutationOrResolveRaceAsync(
            operation,
            keyHash,
            requestHash,
            proposal.Id,
            invocation.CorrelationId,
            cancellationToken);
        if (approvalRace is not null)
            return approvalRace;
        signal.Notify();

        return Result(
            true,
            AgentProposalStates.Provisioning,
            "Owner approval recorded and provisioning queued.",
            proposal,
            invocation.CorrelationId);
    }

    private async Task CreateAgentAsync(
        AgentProvisionRequest request,
        CancellationToken cancellationToken)
    {
        var proposal = request.Proposal;
        var before = await TryReadAgentInventoryAsync(cancellationToken);
        if (!before.Success)
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "inventory_unavailable",
                "The live agent inventory could not be verified before agents.create.",
                cancellationToken);
            return;
        }

        if (before.Items.Any(agent => string.Equals(
                agent.AgentId,
                proposal.RequestedAgentId,
                StringComparison.Ordinal)))
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "agent_exists",
                $"OpenClaw already contains agent '{proposal.RequestedAgentId}'.",
                cancellationToken);
            return;
        }

        string resolvedWorkspace;
        try
        {
            resolvedWorkspace = await ResolveWorkspaceForNewAgentAsync(
                proposal.RequestedAgentId,
                cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "workspace_resolution_cancelled",
                "Provisioning stopped before agents.create while resolving the live OpenClaw workspace configuration.",
                CancellationToken.None);
            return;
        }
        catch (Exception exception)
        {
            logger.LogWarning(
                exception,
                "Live OpenClaw workspace resolution failed for proposal {ProposalId}",
                proposal.Id);
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "workspace_config_unavailable",
                "The live OpenClaw configuration could not be verified before agents.create.",
                CancellationToken.None);
            return;
        }

        if (!string.Equals(
                proposal.Workspace,
                resolvedWorkspace,
                StringComparison.Ordinal))
        {
            proposal.Workspace = resolvedWorkspace;
            Touch(proposal);
            db.OutboxEvents.Add(Event(
                "agent.proposal.workspace_resolved",
                proposal,
                new
                {
                    ProposalId = proposal.Id,
                    proposal.RequestedAgentId
                }));
            await db.SaveChangesAsync(cancellationToken);
        }

        try
        {
            var parameters = new JsonObject
            {
                ["name"] = proposal.RequestedName,
                ["workspace"] = proposal.Workspace
            };
            if (!string.IsNullOrWhiteSpace(proposal.Model))
                parameters["model"] = proposal.Model;
            if (!string.IsNullOrWhiteSpace(proposal.Emoji))
                parameters["emoji"] = proposal.Emoji;
            if (!string.IsNullOrWhiteSpace(proposal.Avatar))
                parameters["avatar"] = proposal.Avatar;

            var response = await connector.InvokeAsync(
                "agents.create",
                parameters,
                timeout: TimeSpan.FromSeconds(30),
                cancellationToken: cancellationToken,
                invocationContext: OpenClawInvocationContext.Create(
                    actor: request.Actor,
                    idempotencyKey: $"agent-provision:{request.Id:N}",
                    correlationId: request.CorrelationId,
                    traceParent: request.TraceParent,
                    includeIdempotencyParameter: false));
            var returnedAgentId = ReadString(response, "agentId");
            var returnedWorkspace = ReadString(response, "workspace");
            if (ReadBool(response, "ok") == false
                || string.IsNullOrWhiteSpace(returnedAgentId))
            {
                throw new OpenClawGatewayRpcException(
                    "INVALID_CREATE_RESPONSE",
                    "OpenClaw did not return a confirmed agent identifier.");
            }

            var readBack = await TryReadAgentInventoryAsync(cancellationToken);
            var verified = readBack.Success
                ? readBack.Items.SingleOrDefault(agent => string.Equals(
                agent.AgentId,
                returnedAgentId,
                StringComparison.Ordinal))
                : null;
            if (verified is null)
            {
                await CompleteFailureAsync(
                    request,
                    AgentProvisionRequestStates.InDoubt,
                    AgentProposalStates.InDoubt,
                    "create_not_visible",
                    "agents.create returned success, but the new agent was not visible in agents.list.",
                    cancellationToken);
                return;
            }

            proposal.OpenClawAgentId = verified.AgentId;
            proposal.OpenClawWorkspace =
                returnedWorkspace ?? verified.Workspace ?? proposal.Workspace;
            request.OpenClawAgentId = verified.AgentId;
            Touch(proposal);
            Touch(request);
            await db.SaveChangesAsync(cancellationToken);
            await FinalizeFilesAsync(request, cancellationToken);
        }
        catch (OperationCanceledException)
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.InDoubt,
                AgentProposalStates.InDoubt,
                "create_dispatch_cancelled",
                "The request crossed the agents.create dispatch boundary before cancellation.",
                CancellationToken.None);
        }
        catch (Exception exception)
        {
            logger.LogWarning(
                exception,
                "OpenClaw agent create did not complete for proposal {ProposalId}",
                proposal.Id);
            var reconciliation = await TryReadAgentInventoryAsync(
                CancellationToken.None);
            var reconciled = reconciliation.Success
                ? reconciliation.Items.SingleOrDefault(item => string.Equals(
                    item.AgentId,
                    proposal.RequestedAgentId,
                    StringComparison.Ordinal))
                : null;
            if (reconciled is not null)
            {
                proposal.OpenClawAgentId = reconciled.AgentId;
                proposal.OpenClawWorkspace = reconciled.Workspace ?? proposal.Workspace;
                request.OpenClawAgentId = reconciled.AgentId;
                Touch(proposal);
                Touch(request);
                await db.SaveChangesAsync(CancellationToken.None);
                await FinalizeFilesAsync(request, CancellationToken.None);
                return;
            }

            var definiteFailure =
                exception is OpenClawGatewayRpcException rpc && !rpc.Retryable;
            await CompleteFailureAsync(
                request,
                definiteFailure
                    ? AgentProvisionRequestStates.Failed
                    : AgentProvisionRequestStates.InDoubt,
                definiteFailure
                    ? AgentProposalStates.Failed
                    : AgentProposalStates.InDoubt,
                exception is OpenClawGatewayRpcException gatewayError
                    ? NormalizeErrorCode(gatewayError.Code)
                    : "create_dispatch_uncertain",
                definiteFailure
                    ? SafeErrorMessage(exception)
                    : "The agents.create outcome is uncertain. Nexus will not repeat it automatically.",
                CancellationToken.None);
        }
    }

    private async Task ReconcileOnlyAsync(
        AgentProvisionRequest request,
        CancellationToken cancellationToken)
    {
        var proposal = request.Proposal;
        var inventory = await TryReadAgentInventoryAsync(cancellationToken);
        if (!inventory.Success)
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.InDoubt,
                AgentProposalStates.InDoubt,
                "reconciliation_unavailable",
                "Read-only reconciliation could not read agents.list. agents.create was not repeated.",
                cancellationToken);
            return;
        }

        var expectedAgentId = proposal.OpenClawAgentId ?? proposal.RequestedAgentId;
        var found = inventory.Items.SingleOrDefault(item => string.Equals(
            item.AgentId,
            expectedAgentId,
            StringComparison.Ordinal));
        if (found is null)
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "reconciled_absent",
                "Read-only reconciliation found no matching OpenClaw agent. agents.create was not repeated.",
                cancellationToken);
            return;
        }

        proposal.OpenClawAgentId = found.AgentId;
        proposal.OpenClawWorkspace = found.Workspace ?? proposal.Workspace;
        request.OpenClawAgentId = found.AgentId;
        Touch(proposal);
        Touch(request);
        await db.SaveChangesAsync(cancellationToken);
        await FinalizeFilesAsync(request, cancellationToken);
    }

    private async Task FinalizeFilesAsync(
        AgentProvisionRequest request,
        CancellationToken cancellationToken)
    {
        var proposal = request.Proposal;
        var agentId = request.OpenClawAgentId ?? proposal.OpenClawAgentId;
        if (string.IsNullOrWhiteSpace(agentId))
        {
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Failed,
                AgentProposalStates.Failed,
                "missing_agent_id",
                "Agent files cannot be finalized without a confirmed OpenClaw agent id.",
                cancellationToken);
            return;
        }

        var files = DeserializeFiles(proposal.StandardFilesJson);
        try
        {
            foreach (var file in files.OrderBy(item => Array.IndexOf(
                         StandardFileNames,
                         item.Key)))
            {
                var current = await agentConfiguration.GetAgentFileAsync(
                    agentId,
                    file.Key,
                    cancellationToken);
                if (!current.Missing
                    && string.Equals(
                        current.ContentHash,
                        Hash(file.Value),
                        StringComparison.Ordinal))
                {
                    continue;
                }

                await agentConfiguration.SetAgentFileAsync(
                    agentId,
                    file.Key,
                    new UpdateOpenClawAgentFileRequest(
                        file.Value,
                        current.ContentHash),
                    OpenClawInvocationContext.Create(
                        actor: request.Actor,
                        idempotencyKey: $"agent-provision:{proposal.Id:N}:{file.Key}",
                        correlationId: request.CorrelationId,
                        traceParent: request.TraceParent,
                        includeIdempotencyParameter: false),
                    cancellationToken);
            }

            proposal.Status = AgentProposalStates.Ready;
            proposal.StandardFilesJson = "{}";
            proposal.LastErrorCode = null;
            proposal.LastErrorMessage = null;
            proposal.CompletedAt = DateTimeOffset.UtcNow;
            request.Status = AgentProvisionRequestStates.Completed;
            request.LastErrorCode = null;
            request.LastErrorMessage = null;
            request.CompletedAt = DateTimeOffset.UtcNow;
            request.LeaseOwner = null;
            request.LeaseUntil = null;
            Touch(proposal);
            Touch(request);
            db.OutboxEvents.Add(Event(
                "agent.provision.completed",
                proposal,
                new
                {
                    ProposalId = proposal.Id,
                    proposal.OpenClawAgentId,
                    proposal.Status,
                    ProvisionRequestId = request.Id
                }));
            await db.SaveChangesAsync(cancellationToken);
        }
        catch (Exception exception)
        {
            logger.LogWarning(
                exception,
                "Agent file finalization failed for proposal {ProposalId}",
                proposal.Id);
            await CompleteFailureAsync(
                request,
                AgentProvisionRequestStates.Partial,
                AgentProposalStates.Partial,
                "file_verification_failed",
                "OpenClaw created the agent, but one or more requested files were not written and verified.",
                CancellationToken.None);
        }
    }

    private async Task CompleteFailureAsync(
        AgentProvisionRequest request,
        string requestState,
        string proposalState,
        string errorCode,
        string errorMessage,
        CancellationToken cancellationToken)
    {
        request.Status = requestState;
        request.LastErrorCode = NormalizeErrorCode(errorCode);
        request.LastErrorMessage = Truncate(errorMessage, 2000);
        request.CompletedAt = DateTimeOffset.UtcNow;
        request.LeaseOwner = null;
        request.LeaseUntil = null;
        request.Proposal.Status = proposalState;
        request.Proposal.LastErrorCode = request.LastErrorCode;
        request.Proposal.LastErrorMessage = request.LastErrorMessage;
        Touch(request);
        Touch(request.Proposal);
        db.OutboxEvents.Add(Event(
            "agent.provision.failed",
            request.Proposal,
            new
            {
                request.ProposalId,
                request.Id,
                RequestStatus = request.Status,
                ProposalStatus = request.Proposal.Status,
                ErrorCode = request.LastErrorCode
            }));
        await db.SaveChangesAsync(cancellationToken);
    }

    private async Task<ProvisioningGate> EvaluateGateAsync(
        CancellationToken cancellationToken)
    {
        var root = NormalizeWorkspaceRoot(provisioningOptions.Value.WorkspaceRoot);
        if (root is null)
        {
            return ProvisioningGate.Blocked(
                "workspace_root_invalid",
                "The server-managed OpenClaw workspace root is invalid.",
                "Configure OpenClawAgentProvisioning:WorkspaceRoot with an absolute or ~/ path.");
        }

        var requiredMethods = new[]
        {
            "agents.list",
            "agents.create",
            "agents.files.get",
            "agents.files.set",
            "config.get"
        };
        foreach (var method in requiredMethods)
        {
            var decision = await writeGate.EvaluateAsync(
                method,
                "operator.admin",
                cancellationToken);
            if (!decision.Allowed)
            {
                return ProvisioningGate.Blocked(
                    decision.State,
                    decision.Message,
                    decision.Recovery);
            }
        }

        return ProvisioningGate.Allowed();
    }

    /// <summary>
    /// Mirrors OpenClaw's workspace rule for a newly created, non-default
    /// agent. A configured agents.defaults.workspace is treated as the parent
    /// directory; otherwise Nexus uses the server-only OpenClaw state root and
    /// OpenClaw's workspace-{agentId} fallback. Browser input is never used.
    /// </summary>
    private async Task<string> ResolveWorkspaceForNewAgentAsync(
        string agentId,
        CancellationToken cancellationToken)
    {
        var snapshot = await agentConfiguration.GetConfigAsync(cancellationToken);
        if (!snapshot.Exists || !snapshot.Valid || snapshot.Config is null)
        {
            throw new InvalidOperationException(
                "OpenClaw config.get did not return a valid live configuration.");
        }

        var configuredDefault = ReadNestedString(
            snapshot.Config,
            "agents",
            "defaults",
            "workspace");
        if (!string.IsNullOrWhiteSpace(configuredDefault))
        {
            var defaultRoot = NormalizeWorkspaceRoot(configuredDefault);
            if (defaultRoot is null)
            {
                throw new InvalidOperationException(
                    "OpenClaw agents.defaults.workspace is not a safe absolute or home-relative path.");
            }

            return $"{defaultRoot.TrimEnd('/')}/{agentId}";
        }

        var stateRoot = NormalizeWorkspaceRoot(
            provisioningOptions.Value.WorkspaceRoot)
            ?? throw new InvalidOperationException(
                "The server-managed OpenClaw workspace root is invalid.");
        return $"{stateRoot.TrimEnd('/')}/workspace-{agentId}";
    }

    public static string BuildCapabilityHash(IGatewayConnector gateway)
        => OpenClawWriteGate.BuildCapabilityHash(gateway);

    private async Task<AgentInventoryRead> TryReadAgentInventoryAsync(
        CancellationToken cancellationToken)
    {
        if (connector.ConnectionState != GatewayConnectionState.Connected
            || !connector.Supports("agents.list")
            || !connector.GrantedScopes.Contains("operator.read"))
        {
            return new AgentInventoryRead(false, []);
        }

        try
        {
            var response = await connector.InvokeAsync(
                "agents.list",
                new JsonObject(),
                cancellationToken: cancellationToken);
            var items = ReadArray(response, "agents", "items")
                .Select(item => new AgentInventoryItem(
                    ReadString(item, "id", "agentId") ?? string.Empty,
                    ReadString(item, "workspace")))
                .Where(item => !string.IsNullOrWhiteSpace(item.AgentId))
                .ToArray();
            return new AgentInventoryRead(true, items);
        }
        catch (Exception exception)
        {
            logger.LogDebug(exception, "OpenClaw agent inventory read failed");
            return new AgentInventoryRead(false, []);
        }
    }

    private async Task<IReadOnlyList<AgentCreateModelOptionDto>> TryListModelsAsync(
        CancellationToken cancellationToken)
    {
        if (connector.ConnectionState != GatewayConnectionState.Connected
            || !connector.Supports("models.list")
            || !connector.GrantedScopes.Contains("operator.read"))
        {
            return [];
        }

        try
        {
            var response = await connector.InvokeAsync(
                "models.list",
                new JsonObject { ["view"] = "configured" },
                cancellationToken: cancellationToken);
            return ReadArray(response, "models", "items")
                .Select(item =>
                {
                    var id = ReadString(item, "id") ?? string.Empty;
                    var provider = ReadString(item, "provider")
                        ?? id.Split('/', 2)[0];
                    return new AgentCreateModelOptionDto(
                        id,
                        ReadString(item, "name") ?? id,
                        provider,
                        ReadBool(item, "available") != false);
                })
                .Where(item => !string.IsNullOrWhiteSpace(item.Id))
                .OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase)
                .ToArray();
        }
        catch (Exception exception)
        {
            logger.LogDebug(exception, "OpenClaw model options read failed");
            return [];
        }
    }

    private async Task<AgentProposalOperationDto?> ReplayAsync(
        string operation,
        string keyHash,
        string requestHash,
        string correlationId,
        CancellationToken cancellationToken)
    {
        var existing = await db.OperationClaims
            .AsNoTracking()
            .SingleOrDefaultAsync(
                item => item.Operation == operation
                        && item.IdempotencyKeyHash == keyHash,
                cancellationToken);
        if (existing is null)
            return null;
        if (!string.Equals(existing.RequestHash, requestHash, StringComparison.Ordinal))
        {
            return new AgentProposalOperationDto(
                false,
                "idempotency_conflict",
                "The Idempotency-Key is already bound to another request.",
                null,
                "Use a new Idempotency-Key for a different operation.",
                correlationId,
                DateTimeOffset.UtcNow);
        }

        AgentProposal? proposal = null;
        if (existing.ResourceId is Guid resourceId)
        {
            proposal = await db.AgentProposals
                .AsNoTracking()
                .SingleOrDefaultAsync(item => item.Id == resourceId, cancellationToken);
        }

        var ok = existing.State == "completed";
        return new AgentProposalOperationDto(
            ok,
            ok ? "idempotent_replay" : existing.ResultCode ?? existing.State,
            ok
                ? "The original operation result was returned without repeating side effects."
                : "The original rejected operation result was returned.",
            proposal is null ? null : Map(proposal, includeFileContent: false),
            null,
            correlationId,
            DateTimeOffset.UtcNow);
    }

    private async Task<AgentProposalOperationDto?> SaveMutationOrResolveRaceAsync(
        string operation,
        string keyHash,
        string requestHash,
        Guid proposalId,
        string correlationId,
        CancellationToken cancellationToken)
    {
        try
        {
            await db.SaveChangesAsync(cancellationToken);
            return null;
        }
        catch (DbUpdateConcurrencyException exception)
        {
            logger.LogInformation(
                exception,
                "Agent proposal {ProposalId} changed during {Operation}",
                proposalId,
                operation);
            return await ResolveMutationRaceAsync(
                operation,
                keyHash,
                requestHash,
                proposalId,
                correlationId,
                cancellationToken);
        }
        catch (DbUpdateException exception)
            when (exception.InnerException is PostgresException
                  {
                      SqlState: PostgresErrorCodes.UniqueViolation
                  })
        {
            logger.LogInformation(
                exception,
                "Agent proposal {ProposalId} encountered a uniqueness race during {Operation}",
                proposalId,
                operation);
            return await ResolveMutationRaceAsync(
                operation,
                keyHash,
                requestHash,
                proposalId,
                correlationId,
                cancellationToken);
        }
    }

    private async Task<AgentProposalOperationDto> ResolveMutationRaceAsync(
        string operation,
        string keyHash,
        string requestHash,
        Guid proposalId,
        string correlationId,
        CancellationToken cancellationToken)
    {
        db.ChangeTracker.Clear();

        var replay = await ReplayAsync(
            operation,
            keyHash,
            requestHash,
            correlationId,
            cancellationToken);
        if (replay is not null)
            return replay;

        var current = await db.AgentProposals
            .AsNoTracking()
            .SingleOrDefaultAsync(
                item => item.Id == proposalId,
                cancellationToken);
        return await RecordFailureClaimAsync(
            operation,
            keyHash,
            requestHash,
            proposalId,
            "concurrency_conflict",
            "Agent proposal changed while the operation was being persisted.",
            correlationId,
            cancellationToken,
            current,
            "Reload the proposal and retry with its current revision.");
    }

    private async Task<AgentProposalOperationDto> RecordFailureClaimAsync(
        string operation,
        string keyHash,
        string requestHash,
        Guid resourceId,
        string state,
        string message,
        string correlationId,
        CancellationToken cancellationToken,
        AgentProposal? proposal = null,
        string? recovery = null)
    {
        db.OperationClaims.Add(Claim(
            operation,
            keyHash,
            requestHash,
            resourceId,
            "rejected",
            state));
        await db.SaveChangesAsync(cancellationToken);
        return new AgentProposalOperationDto(
            false,
            state,
            message,
            proposal is null ? null : Map(proposal, includeFileContent: false),
            recovery,
            correlationId,
            DateTimeOffset.UtcNow);
    }

    private AgentProvisionRequest BuildProvisionRequest(
        AgentProposal proposal,
        string stage,
        OpenClawInvocationMetadata invocation,
        string idempotencyKeyHash)
        => new()
        {
            ProposalId = proposal.Id,
            Proposal = proposal,
            Attempt = proposal.ProvisionRequests.Select(item => item.Attempt).DefaultIfEmpty(0).Max() + 1,
            Stage = stage,
            Status = AgentProvisionRequestStates.Queued,
            IdempotencyKeyHash = idempotencyKeyHash,
            Actor = NormalizeActor(invocation.Actor),
            CorrelationId = Truncate(invocation.CorrelationId, 200),
            TraceParent = NormalizeOptional(invocation.TraceParent, 128),
            CreatedAt = DateTimeOffset.UtcNow,
            UpdatedAt = DateTimeOffset.UtcNow
        };

    private static OperationClaim Claim(
        string operation,
        string keyHash,
        string requestHash,
        Guid? resourceId,
        string state,
        string? resultCode)
        => new()
        {
            Operation = operation,
            IdempotencyKeyHash = keyHash,
            RequestHash = requestHash,
            ResourceId = resourceId,
            State = state,
            ResultCode = resultCode,
            CompletedAt = DateTimeOffset.UtcNow,
            ExpiresAt = DateTimeOffset.UtcNow.AddDays(7)
        };

    private static OutboxEvent Event(
        string type,
        AgentProposal proposal,
        object payload)
        => new()
        {
            Type = type,
            AggregateType = "agent-proposal",
            AggregateId = proposal.Id.ToString("N"),
            AggregateRevision = proposal.Revision,
            PayloadJson = JsonSerializer.Serialize(payload, InternalJsonOptions),
            OccurredAt = DateTimeOffset.UtcNow
        };

    private static AgentProposalOperationDto Result(
        bool ok,
        string state,
        string message,
        AgentProposal proposal,
        string correlationId,
        string? recovery = null)
        => new(
            ok,
            state,
            message,
            Map(proposal, includeFileContent: false),
            recovery,
            correlationId,
            DateTimeOffset.UtcNow,
            new OperationResultDto(
                correlationId,
                state,
                proposal.Revision,
                new EntityRefDto(
                    "agent-proposal",
                    proposal.Id.ToString(),
                    proposal.RequestedName),
                [],
                Activity.Current?.Id));

    private static AgentProposalDto Map(
        AgentProposal proposal,
        bool includeFileContent)
    {
        var files = DeserializeFiles(proposal.StandardFilesJson)
            .OrderBy(item => Array.IndexOf(StandardFileNames, item.Key))
            .Select(item => new AgentProposalFileDto(
                item.Key,
                Hash(item.Value),
                Encoding.UTF8.GetByteCount(item.Value),
                includeFileContent ? item.Value : null))
            .ToArray();
        var error = string.IsNullOrWhiteSpace(proposal.LastErrorCode)
            ? null
            : new AgentProposalErrorDto(
                proposal.LastErrorCode,
                proposal.LastErrorMessage ?? "Agent provisioning failed.",
                RecoveryFor(proposal.LastErrorCode));

        return new AgentProposalDto(
            proposal.Id,
            proposal.Source,
            proposal.RequestedName,
            proposal.RequestedAgentId,
            proposal.Role,
            proposal.Description,
            proposal.Model,
            proposal.Emoji,
            proposal.Avatar,
            proposal.Workspace,
            files,
            proposal.Status,
            proposal.RequestedBy,
            proposal.ApprovedBy,
            proposal.RejectedBy,
            proposal.RejectionReason,
            proposal.OpenClawAgentId,
            proposal.OpenClawWorkspace,
            error,
            proposal.Revision,
            proposal.CreatedAt,
            proposal.UpdatedAt,
            proposal.ApprovedAt,
            proposal.RejectedAt,
            proposal.CompletedAt);
    }

    private NormalizedProposal NormalizeProposal(CreateAgentProposalRequest request)
    {
        var name = NormalizeRequired(request.Name, nameof(request.Name), MaxNameLength);
        var agentId = NormalizeAgentId(name);
        if (agentId == "main")
        {
            throw new AgentProposalValidationException(
                nameof(request.Name),
                "'main' is reserved by OpenClaw.");
        }

        var role = NormalizeOptional(request.Role, MaxRoleLength);
        var description = NormalizeOptional(request.Description, MaxDescriptionLength);
        var model = NormalizeOptional(request.Model, MaxModelLength);
        if (model is not null && !SafeModelPattern().IsMatch(model))
        {
            throw new AgentProposalValidationException(
                nameof(request.Model),
                "Model must be a provider/model style identifier.");
        }

        var emoji = NormalizeOptional(request.Emoji, 32);
        var avatar = NormalizeOptional(request.Avatar, 2048);
        if (avatar?.StartsWith("data:", StringComparison.OrdinalIgnoreCase) == true)
        {
            throw new AgentProposalValidationException(
                nameof(request.Avatar),
                "Inline data avatars are not accepted in proposals.");
        }

        RejectLikelySecret(role, nameof(request.Role));
        RejectLikelySecret(description, nameof(request.Description));
        var files = new SortedDictionary<string, string>(StringComparer.Ordinal);
        foreach (var entry in request.Files ?? new Dictionary<string, string>())
        {
            if (!CanonicalFileNames.TryGetValue(entry.Key?.Trim() ?? string.Empty, out var nameKey))
            {
                throw new AgentProposalValidationException(
                    nameof(request.Files),
                    $"'{entry.Key}' is not an editable OpenClaw standard file.");
            }

            var size = Encoding.UTF8.GetByteCount(entry.Value ?? string.Empty);
            if (size > Math.Clamp(
                    provisioningOptions.Value.MaxProposalFileBytes,
                    1024,
                    1_048_576))
            {
                throw new AgentProposalValidationException(
                    nameof(request.Files),
                    $"{nameKey} exceeds the configured proposal file limit.");
            }

            RejectLikelySecret(entry.Value, $"{nameof(request.Files)}.{nameKey}");
            files[nameKey] = entry.Value ?? string.Empty;
        }

        if (!files.ContainsKey("IDENTITY.md"))
        {
            files["IDENTITY.md"] = BuildIdentityFile(name, role, emoji);
        }

        if (!files.ContainsKey("AGENTS.md") && (role is not null || description is not null))
        {
            files["AGENTS.md"] = BuildStandingOrdersFile(role, description);
        }

        var totalBytes = files.Sum(entry => Encoding.UTF8.GetByteCount(entry.Value));
        if (totalBytes > Math.Clamp(
                provisioningOptions.Value.MaxProposalFilesBytes,
                4096,
                4_194_304))
        {
            throw new AgentProposalValidationException(
                nameof(request.Files),
                "Combined proposal files exceed the configured limit.");
        }

        var filesJson = JsonSerializer.Serialize(files, InternalJsonOptions);
        var workspaceRoot = NormalizeWorkspaceRoot(
            provisioningOptions.Value.WorkspaceRoot)
            ?? throw new AgentProposalValidationException(
                "workspaceRoot",
                "The server-managed workspace root is invalid.");
        var workspace = $"{workspaceRoot.TrimEnd('/')}/workspace-{agentId}";

        return new NormalizedProposal(
            name,
            agentId,
            role,
            description,
            model,
            emoji,
            avatar,
            workspace,
            filesJson,
            Hash(filesJson));
    }

    private static string BuildIdentityFile(
        string name,
        string? role,
        string? emoji)
    {
        var lines = new List<string>
        {
            "# Identity",
            string.Empty,
            $"- Name: {name}"
        };
        if (!string.IsNullOrWhiteSpace(role))
            lines.Add($"- Role: {role}");
        if (!string.IsNullOrWhiteSpace(emoji))
            lines.Add($"- Emoji: {emoji}");
        return string.Join("\n", lines) + "\n";
    }

    private static string BuildStandingOrdersFile(
        string? role,
        string? description)
    {
        var mission = description ?? role ?? "Follow the owner's explicit mission.";
        return $"""
            # Standing Orders

            ## Mission

            {mission}

            ## Approval boundaries

            - Ask the owner before destructive, irreversible, credential, deployment, or external communication actions.
            - Stay within the assigned workspace and granted OpenClaw capabilities.

            ## Verify and report

            - Verify material results before reporting completion.
            - Report blockers and uncertain side effects explicitly.
            """.Trim() + "\n";
    }

    private static void RejectLikelySecret(string? value, string field)
    {
        if (string.IsNullOrWhiteSpace(value))
            return;
        if (PrivateKeyPattern().IsMatch(value)
            || HighConfidenceTokenPattern().IsMatch(value))
        {
            throw new AgentProposalValidationException(
                field,
                "Proposal content must not contain credentials or private keys.");
        }
    }

    private static string NormalizeSource(string source)
    {
        var normalized = source?.Trim().ToLowerInvariant();
        return normalized switch
        {
            "manual" or "iris" or "mcp" => normalized,
            _ => throw new AgentProposalValidationException(
                nameof(source),
                "Proposal source must be manual, iris, or mcp.")
        };
    }

    private static string NormalizeAgentId(string value)
    {
        var trimmed = value.Trim();
        if (ValidAgentIdPattern().IsMatch(trimmed))
            return trimmed.ToLowerInvariant();

        var normalized = InvalidAgentIdCharactersPattern()
            .Replace(trimmed.ToLowerInvariant(), "-");
        normalized = LeadingDashesPattern().Replace(normalized, string.Empty);
        normalized = TrailingDashesPattern().Replace(normalized, string.Empty);
        if (normalized.Length > 64)
            normalized = normalized[..64];
        return string.IsNullOrWhiteSpace(normalized) ? "main" : normalized;
    }

    private static string? NormalizeWorkspaceRoot(string? root)
    {
        var normalized = root?.Trim().Replace('\\', '/').TrimEnd('/');
        if (string.IsNullOrWhiteSpace(normalized)
            || normalized.Length > 1024
            || normalized.Contains('\0')
            || normalized.Split('/', StringSplitOptions.RemoveEmptyEntries)
                .Any(segment => segment == ".."))
        {
            return null;
        }

        var rooted = normalized.StartsWith("/", StringComparison.Ordinal)
                     || normalized.StartsWith("~/", StringComparison.Ordinal)
                     || WindowsRootPattern().IsMatch(normalized);
        return rooted ? normalized : null;
    }

    private static string? ReadNestedString(
        JsonNode root,
        params string[] path)
    {
        JsonNode? current = root;
        foreach (var segment in path)
        {
            current = current is JsonObject obj
                ? obj[segment]
                : null;
            if (current is null)
                return null;
        }

        try
        {
            return current.GetValue<string>()?.Trim();
        }
        catch (InvalidOperationException)
        {
            return null;
        }
    }

    private static string NormalizeRequired(
        string? value,
        string field,
        int maxLength)
    {
        var normalized = value?.Trim();
        if (string.IsNullOrWhiteSpace(normalized)
            || normalized.Length > maxLength
            || normalized.Contains('\0')
            || normalized.Contains('\r')
            || normalized.Contains('\n'))
        {
            throw new AgentProposalValidationException(
                field,
                $"{field} is required, single-line and limited to {maxLength} characters.");
        }

        return normalized;
    }

    private static string? NormalizeOptional(string? value, int maxLength)
    {
        var normalized = value?.Trim();
        if (string.IsNullOrWhiteSpace(normalized))
            return null;
        if (normalized.Length > maxLength || normalized.Contains('\0'))
        {
            throw new AgentProposalValidationException(
                "value",
                $"Value is limited to {maxLength} characters.");
        }

        return normalized;
    }

    private static string NormalizeActor(string? actor)
        => Truncate(
            string.IsNullOrWhiteSpace(actor)
                ? "authenticated-user"
                : actor.Trim(),
            240);

    private static void ValidateInvocation(OpenClawInvocationMetadata invocation)
    {
        ArgumentNullException.ThrowIfNull(invocation);
        ValidateIdempotencyKey(invocation.IdempotencyKey);
        if (string.IsNullOrWhiteSpace(invocation.CorrelationId)
            || invocation.CorrelationId.Length > 200)
        {
            throw new AgentProposalValidationException(
                "X-Correlation-ID",
                "A correlation id with at most 200 characters is required.");
        }
    }

    private static void ValidateIdempotencyKey(string? key)
    {
        if (string.IsNullOrWhiteSpace(key) || key.Length > MaxIdempotencyKeyLength)
        {
            throw new AgentProposalValidationException(
                "Idempotency-Key",
                $"A non-empty idempotency key with at most {MaxIdempotencyKeyLength} characters is required.");
        }
    }

    private static void ValidateReason(string? reason)
    {
        if (reason?.Length > 1000 || reason?.Contains('\0') == true)
        {
            throw new AgentProposalValidationException(
                nameof(reason),
                "Reason must contain at most 1000 characters.");
        }
    }

    private static void Touch(AgentProposal proposal)
    {
        proposal.Revision++;
        proposal.UpdatedAt = DateTimeOffset.UtcNow;
    }

    private static void Touch(AgentProvisionRequest request)
    {
        request.Revision++;
        request.UpdatedAt = DateTimeOffset.UtcNow;
    }

    private static Dictionary<string, string> DeserializeFiles(string json)
    {
        try
        {
            return JsonSerializer.Deserialize<Dictionary<string, string>>(
                       json,
                       InternalJsonOptions)
                   ?? [];
        }
        catch (JsonException)
        {
            return [];
        }
    }

    private static IReadOnlyList<JsonNode> ReadArray(
        JsonNode? node,
        params string[] propertyNames)
    {
        if (node is JsonArray direct)
            return direct.Where(item => item is not null).Select(item => item!).ToArray();
        if (node is not JsonObject obj)
            return [];

        foreach (var name in propertyNames)
        {
            if (obj[name] is JsonArray array)
                return array.Where(item => item is not null).Select(item => item!).ToArray();
        }

        return [];
    }

    private static string? ReadString(JsonNode? node, params string[] propertyNames)
    {
        if (node is not JsonObject obj)
            return null;
        foreach (var name in propertyNames)
        {
            if (obj[name] is not JsonNode value)
                continue;
            try
            {
                var text = value.GetValue<string>()?.Trim();
                if (!string.IsNullOrWhiteSpace(text))
                    return text;
            }
            catch
            {
            }
        }

        return null;
    }

    private static bool? ReadBool(JsonNode? node, params string[] propertyNames)
    {
        if (node is not JsonObject obj)
            return null;
        foreach (var name in propertyNames)
        {
            try
            {
                if (obj[name] is JsonNode value)
                    return value.GetValue<bool>();
            }
            catch
            {
            }
        }

        return null;
    }

    private static string Hash(string value)
        => Convert.ToHexString(
                SHA256.HashData(Encoding.UTF8.GetBytes(value)))
            .ToLowerInvariant();

    private static string SafeErrorMessage(Exception exception)
        => Truncate(
            exception is OpenClawGatewayRpcException gateway
                ? $"{gateway.Code}: {gateway.Message}"
                : exception.Message,
            2000);

    private static string NormalizeErrorCode(string value)
    {
        var normalized = new string(value
            .Trim()
            .ToLowerInvariant()
            .Select(character =>
                char.IsAsciiLetterOrDigit(character) || character == '_'
                    ? character
                    : '_')
            .Take(120)
            .ToArray());
        return string.IsNullOrWhiteSpace(normalized) ? "unknown_error" : normalized;
    }

    private static string Truncate(string value, int length)
        => value.Length <= length ? value : value[..length];

    private static string? RecoveryFor(string errorCode)
        => errorCode switch
        {
            "experimental_blocked" =>
                "Wait for an officially supported external Nexus client identity.",
            "management_disabled" =>
                "An owner must enable management for the adopted primary profile.",
            "scope_upgrade_required" =>
                "Approve operator.admin in OpenClaw and re-verify the connection.",
            "capability_drift" =>
                "Re-verify the OpenClaw profile before issuing another write.",
            "create_dispatch_uncertain" or "create_dispatch_cancelled"
                or "process_interrupted" =>
                "Request a read-only reconciliation. Do not call agents.create again yet.",
            "file_verification_failed" =>
                "Retry file finalization; the OpenClaw agent must not be recreated.",
            _ => null
        };

    private static string EncodeCursor(DateTimeOffset createdAt, Guid id)
        => Convert.ToBase64String(
            Encoding.UTF8.GetBytes(
                $"{createdAt.UtcTicks.ToString(CultureInfo.InvariantCulture)}|{id:N}"));

    private static bool TryDecodeCursor(
        string value,
        out DateTimeOffset beforeCreatedAt,
        out Guid? beforeId)
    {
        beforeCreatedAt = default;
        beforeId = null;
        try
        {
            var decoded = Encoding.UTF8.GetString(Convert.FromBase64String(value));
            var parts = decoded.Split('|', 2);
            if (!long.TryParse(
                    parts[0],
                    NumberStyles.None,
                    CultureInfo.InvariantCulture,
                    out var ticks))
                return false;

            if (parts.Length == 2)
            {
                if (!Guid.TryParseExact(parts[1], "N", out var id))
                    return false;
                beforeId = id;
            }

            beforeCreatedAt = new DateTimeOffset(ticks, TimeSpan.Zero);
            return true;
        }
        catch
        {
            return false;
        }
    }

    [GeneratedRegex("^[a-z0-9][a-z0-9_-]{0,63}$", RegexOptions.IgnoreCase)]
    private static partial Regex ValidAgentIdPattern();

    [GeneratedRegex("[^a-z0-9_-]+")]
    private static partial Regex InvalidAgentIdCharactersPattern();

    [GeneratedRegex("^-+")]
    private static partial Regex LeadingDashesPattern();

    [GeneratedRegex("-+$")]
    private static partial Regex TrailingDashesPattern();

    [GeneratedRegex("^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,239}$")]
    private static partial Regex SafeModelPattern();

    [GeneratedRegex("^(?:/|~/|[a-zA-Z]:/)")]
    private static partial Regex WindowsRootPattern();

    [GeneratedRegex(
        "-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----",
        RegexOptions.IgnoreCase)]
    private static partial Regex PrivateKeyPattern();

    [GeneratedRegex(
        @"(?<![a-zA-Z0-9])(?:sk-[a-zA-Z0-9_-]{20,}|gh[pousr]_[a-zA-Z0-9]{20,}|xox[baprs]-[a-zA-Z0-9-]{20,})(?![a-zA-Z0-9])",
        RegexOptions.IgnoreCase)]
    private static partial Regex HighConfidenceTokenPattern();

    private sealed record NormalizedProposal(
        string Name,
        string AgentId,
        string? Role,
        string? Description,
        string? Model,
        string? Emoji,
        string? Avatar,
        string Workspace,
        string FilesJson,
        string FilesHash);

    private sealed record AgentInventoryItem(
        string AgentId,
        string? Workspace);

    private sealed record AgentInventoryRead(
        bool Success,
        IReadOnlyList<AgentInventoryItem> Items);

    private sealed record ProvisioningGate(
        bool Ok,
        string State,
        string? Message,
        string? Recovery)
    {
        public static ProvisioningGate Allowed()
            => new(true, "ready", null, null);

        public static ProvisioningGate Blocked(
            string state,
            string message,
            string? recovery)
            => new(false, state, message, recovery);
    }
}
