using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Nexus.Api.Models;

namespace Nexus.Api.Services;

/// <summary>
/// Browser-safe façade for OpenClaw-owned agent workspace and configuration data.
/// Nexus never resolves or opens an OpenClaw host path here; all access goes through
/// the authenticated Gateway RPC connection.
/// </summary>
public sealed class OpenClawAgentConfigurationService(
    IGatewayConnector connector,
    IOpenClawOperationAuditStore auditStore,
    IOpenClawWriteGate writeGate,
    ILogger<OpenClawAgentConfigurationService> logger)
    : IOpenClawAgentConfigurationService
{
    public const string MissingContentHash = "missing";

    private const int MaxAgentIdLength = 128;
    private const int MaxAgentFileBytes = 1_048_576;
    private const int MaxConfigPatchBytes = 1_048_576;
    private const int MaxWorkspacePathLength = 1024;
    private const int MaxConfigPathLength = 1024;

    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 string[] SensitiveWorkspaceNameFragments =
    [
        "auth-profile",
        "credential",
        "private-key",
        "private_key",
        "secret",
        "token"
    ];

    private static readonly string[] SensitiveConfigKeyFragments =
    [
        "access-token",
        "access_token",
        "accesstoken",
        "api-key",
        "api_key",
        "apikey",
        "auth-token",
        "auth_token",
        "authtoken",
        "credential",
        "password",
        "private-key",
        "private_key",
        "privatekey",
        "refresh-token",
        "refresh_token",
        "refreshtoken",
        "secret",
        "token"
    ];

    public async Task<OpenClawAgentFileCollectionDto> GetAgentFilesAsync(
        string agentId,
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("agents.files.list", "operator.read");
        var normalizedAgentId = NormalizeAgentId(agentId);
        var response = await connector.InvokeAsync(
            "agents.files.list",
            new JsonObject { ["agentId"] = normalizedAgentId },
            cancellationToken: cancellationToken);

        var files = ReadArray(response, "files")
            .Select(MapAgentFileSummary)
            .Where(file => file is not null)
            .Select(file => file!)
            .OrderBy(file => Array.IndexOf(StandardFileNames, file.Name))
            .ToArray();

        return new OpenClawAgentFileCollectionDto(
            normalizedAgentId,
            files,
            DateTimeOffset.UtcNow);
    }

    public Task<OpenClawAgentFileDto> GetAgentFileAsync(
        string agentId,
        string fileName,
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("agents.files.get", "operator.read");
        return GetAgentFileCoreAsync(
            NormalizeAgentId(agentId),
            NormalizeStandardFileName(fileName),
            cancellationToken);
    }

    public async Task<OpenClawAgentFileWriteDto> SetAgentFileAsync(
        string agentId,
        string fileName,
        UpdateOpenClawAgentFileRequest request,
        OpenClawInvocationContext invocationContext,
        CancellationToken cancellationToken = default)
    {
        await EnsureWriteAvailableAsync(
            "agents.files.set",
            cancellationToken);
        var normalizedAgentId = NormalizeAgentId(agentId);
        var normalizedFileName = NormalizeStandardFileName(fileName);
        ValidateAgentFileWrite(request);
        var expectedHash = NormalizeExpectedHash(request.ExpectedHash);
        var desiredHash = HashContent(request.Content);
        var descriptor = new OpenClawOperationDescriptor(
            "agents.files.set",
            "agent-file",
            $"{normalizedAgentId}/{normalizedFileName}",
            HashContent(
                $"agents.files.set\n{normalizedAgentId}\n{normalizedFileName}\n{expectedHash}\n{desiredHash}"));
        var claim = await auditStore.ClaimAsync(
            invocationContext,
            descriptor,
            cancellationToken);

        if (claim.Disposition != OpenClawOperationClaimDisposition.Started)
        {
            if (claim.Disposition == OpenClawOperationClaimDisposition.Replayed &&
                claim.PreviousOk == true)
            {
                var replayed = await GetAgentFileCoreAsync(
                    normalizedAgentId,
                    normalizedFileName,
                    cancellationToken);
                if (!replayed.Missing &&
                    string.Equals(replayed.ContentHash, desiredHash, StringComparison.Ordinal))
                {
                    return BuildAgentFileWriteResult(
                        replayed,
                        invocationContext,
                        state: "replayed",
                        message: "Die bereits bestätigte Dateiänderung wurde nicht erneut ausgeführt.");
                }

                throw new OpenClawAgentConfigurationConflictException(
                    "idempotency_state_drift",
                    "Die frühere Änderung war erfolgreich, die Datei hat sich danach jedoch erneut geändert.",
                    desiredHash,
                    replayed.ContentHash);
            }

            throw BuildIdempotencyConflict(claim);
        }

        var auditCompleted = false;
        try
        {
            var current = await GetAgentFileCoreAsync(
                normalizedAgentId,
                normalizedFileName,
                cancellationToken);
            if (!string.Equals(current.ContentHash, expectedHash, StringComparison.OrdinalIgnoreCase))
            {
                await CompleteAuditAsync(
                    invocationContext,
                    descriptor,
                    ok: false,
                    state: "conflict",
                    message: "Datei wurde seit dem letzten Lesen verändert.",
                    errorCode: "content_hash_mismatch",
                    cancellationToken);
                auditCompleted = true;
                throw new OpenClawAgentConfigurationConflictException(
                    "content_hash_mismatch",
                    "Die Datei wurde seit dem letzten Lesen verändert.",
                    expectedHash,
                    current.ContentHash);
            }

            var setResponse = await connector.InvokeAsync(
                "agents.files.set",
                new JsonObject
                {
                    ["agentId"] = normalizedAgentId,
                    ["name"] = normalizedFileName,
                    ["content"] = request.Content
                },
                cancellationToken: cancellationToken,
                invocationContext: invocationContext with { IncludeIdempotencyParameter = false });
            if (ReadBool(setResponse, "ok") == false)
            {
                throw new OpenClawAgentConfigurationVerificationException(
                    "OpenClaw hat die Dateiänderung nicht bestätigt.");
            }

            var verified = await GetAgentFileCoreAsync(
                normalizedAgentId,
                normalizedFileName,
                cancellationToken);
            if (verified.Missing ||
                !string.Equals(verified.ContentHash, desiredHash, StringComparison.Ordinal) ||
                !string.Equals(verified.Content, request.Content, StringComparison.Ordinal))
            {
                throw new OpenClawAgentConfigurationVerificationException(
                    "Die Datei konnte nach dem Schreiben nicht identisch zurückgelesen werden.");
            }

            await CompleteAuditAsync(
                invocationContext,
                descriptor,
                ok: true,
                state: "completed",
                message: "Datei gespeichert und identisch zurückgelesen.",
                errorCode: null,
                cancellationToken);
            auditCompleted = true;
            return BuildAgentFileWriteResult(
                verified,
                invocationContext,
                state: "completed",
                message: "Datei gespeichert und identisch zurückgelesen.");
        }
        catch
        {
            if (!auditCompleted)
            {
                await TryCompleteFailedAuditAsync(
                    invocationContext,
                    descriptor,
                    "in_doubt",
                    "Dateiänderung konnte nicht vollständig verifiziert werden.");
            }

            throw;
        }
    }

    public async Task<OpenClawWorkspaceCollectionDto> GetWorkspaceAsync(
        string agentId,
        string? path,
        int offset,
        int limit,
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("agents.workspace.list", "operator.read");
        var normalizedAgentId = NormalizeAgentId(agentId);
        var normalizedPath = NormalizeWorkspacePath(path, allowEmpty: true);
        if (offset < 0)
            throw new OpenClawAgentConfigurationValidationException("offset", "Offset must not be negative.");
        if (limit is < 1 or > 500)
            throw new OpenClawAgentConfigurationValidationException("limit", "Limit must be between 1 and 500.");

        var parameters = new JsonObject
        {
            ["agentId"] = normalizedAgentId,
            ["path"] = normalizedPath,
            ["offset"] = offset,
            ["limit"] = limit
        };
        var response = await connector.InvokeAsync(
            "agents.workspace.list",
            parameters,
            cancellationToken: cancellationToken);

        var entries = ReadArray(response, "entries")
            .Select(MapWorkspaceEntry)
            .Where(entry => entry is not null)
            .Select(entry => entry!)
            .ToArray();
        var responsePath = ReadString(response, "path");
        if (!TryNormalizeWorkspacePath(responsePath, allowEmpty: true, out var safeResponsePath))
            safeResponsePath = normalizedPath;
        var parentPath = ReadString(response, "parentPath");
        if (!TryNormalizeWorkspacePath(parentPath, allowEmpty: true, out var safeParentPath))
            safeParentPath = null;

        return new OpenClawWorkspaceCollectionDto(
            normalizedAgentId,
            safeResponsePath!,
            string.IsNullOrEmpty(safeParentPath) ? null : safeParentPath,
            entries,
            ReadInt(response, "totalEntries") ?? entries.Length,
            ReadInt(response, "offset") ?? offset,
            DateTimeOffset.UtcNow);
    }

    public async Task<OpenClawWorkspaceFileDto> GetWorkspaceFileAsync(
        string agentId,
        string path,
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("agents.workspace.get", "operator.read");
        var normalizedAgentId = NormalizeAgentId(agentId);
        var normalizedPath = NormalizeWorkspacePath(path, allowEmpty: false);
        var response = await connector.InvokeAsync(
            "agents.workspace.get",
            new JsonObject
            {
                ["agentId"] = normalizedAgentId,
                ["path"] = normalizedPath
            },
            cancellationToken: cancellationToken);
        var file = response?["file"]
            ?? throw new OpenClawAgentConfigurationVerificationException(
                "OpenClaw hat keine Workspace-Datei zurückgegeben.");
        var responsePath = ReadString(file, "path");
        if (!TryNormalizeWorkspacePath(responsePath, allowEmpty: false, out var safeResponsePath) ||
            !string.Equals(safeResponsePath, normalizedPath, StringComparison.Ordinal))
        {
            throw new OpenClawAgentConfigurationVerificationException(
                "OpenClaw hat eine Datei außerhalb des angefragten Workspace-Pfads zurückgegeben.");
        }

        var content = ReadString(file, "content") ?? string.Empty;
        var encoding = ReadString(file, "encoding") ?? "utf8";
        if (encoding is not ("utf8" or "base64"))
        {
            throw new OpenClawAgentConfigurationVerificationException(
                "OpenClaw hat eine nicht unterstützte Workspace-Kodierung zurückgegeben.");
        }

        return new OpenClawWorkspaceFileDto(
            normalizedAgentId,
            safeResponsePath!,
            ReadString(file, "name") ?? Path.GetFileName(normalizedPath),
            ReadLong(file, "size") ?? Encoding.UTF8.GetByteCount(content),
            FromUnixMilliseconds(ReadLong(file, "updatedAtMs")),
            ReadString(file, "mimeType") ?? "text/plain",
            encoding,
            content,
            HashContent(content),
            DateTimeOffset.UtcNow);
    }

    public async Task<OpenClawConfigSchemaLookupDto> GetConfigSchemaAsync(
        string path,
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("config.schema.lookup", "operator.read");
        var normalizedPath = NormalizeConfigPath(path, "path");
        var response = await connector.InvokeAsync(
            "config.schema.lookup",
            new JsonObject { ["path"] = normalizedPath },
            cancellationToken: cancellationToken);
        var children = ReadArray(response, "children")
            .Select(child => new OpenClawConfigSchemaChildDto(
                ReadString(child, "key") ?? string.Empty,
                ReadString(child, "path") ?? string.Empty,
                SanitizePayload(child?["type"]),
                ReadBool(child, "required") ?? false,
                ReadBool(child, "hasChildren") ?? false,
                ReadString(child, "reloadKind"),
                SanitizePayload(child?["hint"])))
            .Where(child =>
                !string.IsNullOrWhiteSpace(child.Key) &&
                !string.IsNullOrWhiteSpace(child.Path))
            .ToArray();

        return new OpenClawConfigSchemaLookupDto(
            ReadString(response, "path") ?? normalizedPath,
            SanitizePayload(response?["schema"]),
            ReadString(response, "reloadKind"),
            SanitizePayload(response?["hint"]),
            children,
            DateTimeOffset.UtcNow);
    }

    public Task<OpenClawConfigSnapshotDto> GetConfigAsync(
        CancellationToken cancellationToken = default)
    {
        EnsureAvailable("config.get", "operator.read");
        return GetConfigCoreAsync(cancellationToken);
    }

    public async Task<OpenClawConfigPatchDto> PatchConfigAsync(
        PatchOpenClawConfigRequest request,
        OpenClawInvocationContext invocationContext,
        CancellationToken cancellationToken = default)
    {
        await EnsureWriteAvailableAsync(
            "config.patch",
            cancellationToken);
        ValidateConfigPatch(request);
        var baseHash = NormalizeContentHash(request.BaseHash, "baseHash", allowMissing: false);
        var raw = request.Patch.ToJsonString(new JsonSerializerOptions
        {
            WriteIndented = false
        });
        if (Encoding.UTF8.GetByteCount(raw) > MaxConfigPatchBytes)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "patch",
                $"Config patch must not exceed {MaxConfigPatchBytes} UTF-8 bytes.");
        }

        var replacePaths = NormalizeReplacePaths(request.ReplacePaths);
        var descriptor = new OpenClawOperationDescriptor(
            "config.patch",
            "openclaw-config",
            "primary",
            HashContent(
                $"config.patch\n{baseHash}\n{HashContent(raw)}\n{string.Join('\n', replacePaths)}"));
        var claim = await auditStore.ClaimAsync(
            invocationContext,
            descriptor,
            cancellationToken);

        if (claim.Disposition != OpenClawOperationClaimDisposition.Started)
        {
            if (claim.Disposition == OpenClawOperationClaimDisposition.Replayed &&
                claim.PreviousOk == true)
            {
                var replayed = await GetConfigCoreAsync(cancellationToken);
                return BuildConfigPatchResult(
                    replayed,
                    restart: null,
                    invocationContext,
                    state: "replayed",
                    message: "Die bereits protokollierte Konfigurationsänderung wurde nicht erneut ausgeführt. Der aktuelle Snapshot wurde geladen, das ursprüngliche Read-back lässt sich aus dem metadata-only Audit nicht erneut beweisen.",
                    verified: false);
            }

            throw BuildIdempotencyConflict(claim);
        }

        var auditCompleted = false;
        try
        {
            var before = await GetConfigCoreAsync(cancellationToken);
            if (string.IsNullOrWhiteSpace(before.Hash) ||
                !string.Equals(before.Hash, baseHash, StringComparison.OrdinalIgnoreCase))
            {
                await CompleteAuditAsync(
                    invocationContext,
                    descriptor,
                    ok: false,
                    state: "conflict",
                    message: "OpenClaw-Konfiguration wurde seit dem letzten Lesen verändert.",
                    errorCode: "base_hash_mismatch",
                    cancellationToken);
                auditCompleted = true;
                throw new OpenClawAgentConfigurationConflictException(
                    "base_hash_mismatch",
                    "OpenClaw-Konfiguration wurde seit dem letzten Lesen verändert.",
                    baseHash,
                    before.Hash);
            }

            var parameters = new JsonObject
            {
                ["raw"] = raw,
                ["baseHash"] = baseHash
            };
            if (replacePaths.Count > 0)
                parameters["replacePaths"] = new JsonArray(
                    replacePaths.Select(path => JsonValue.Create(path)).ToArray());
            if (!string.IsNullOrWhiteSpace(request.Note))
                parameters["note"] = request.Note.Trim();
            if (request.RestartDelayMs is { } restartDelayMs)
                parameters["restartDelayMs"] = restartDelayMs;

            var patchResponse = await connector.InvokeAsync(
                "config.patch",
                parameters,
                cancellationToken: cancellationToken,
                invocationContext: invocationContext with { IncludeIdempotencyParameter = false });
            if (ReadBool(patchResponse, "ok") == false)
            {
                throw new OpenClawAgentConfigurationVerificationException(
                    "OpenClaw hat die Konfigurationsänderung nicht bestätigt.");
            }

            var after = await GetConfigCoreAsync(cancellationToken);
            var noop = ReadBool(patchResponse, "noop") == true;
            if (!after.Valid ||
                string.IsNullOrWhiteSpace(after.Hash) ||
                (!noop && string.Equals(after.Hash, before.Hash, StringComparison.OrdinalIgnoreCase)))
            {
                throw new OpenClawAgentConfigurationVerificationException(
                    "Die geänderte OpenClaw-Konfiguration konnte nicht gültig zurückgelesen werden.");
            }

            await CompleteAuditAsync(
                invocationContext,
                descriptor,
                ok: true,
                state: noop ? "noop" : "completed",
                message: noop
                    ? "Konfiguration war bereits aktuell und wurde gültig zurückgelesen."
                    : "Konfiguration geändert und gültig zurückgelesen.",
                errorCode: null,
                cancellationToken);
            auditCompleted = true;
            return BuildConfigPatchResult(
                after,
                SanitizePayload(patchResponse?["restart"]),
                invocationContext,
                noop ? "noop" : "completed",
                noop
                    ? "Konfiguration war bereits aktuell und wurde gültig zurückgelesen."
                    : "Konfiguration geändert und gültig zurückgelesen.");
        }
        catch
        {
            if (!auditCompleted)
            {
                await TryCompleteFailedAuditAsync(
                    invocationContext,
                    descriptor,
                    "in_doubt",
                    "Konfigurationsänderung konnte nicht vollständig verifiziert werden.");
            }

            throw;
        }
    }

    private async Task<OpenClawAgentFileDto> GetAgentFileCoreAsync(
        string agentId,
        string fileName,
        CancellationToken cancellationToken)
    {
        var response = await connector.InvokeAsync(
            "agents.files.get",
            new JsonObject
            {
                ["agentId"] = agentId,
                ["name"] = fileName
            },
            cancellationToken: cancellationToken);
        var file = response?["file"]
            ?? throw new OpenClawAgentConfigurationVerificationException(
                "OpenClaw hat keine Agent-Datei zurückgegeben.");
        var responseName = NormalizeStandardFileName(ReadString(file, "name") ?? fileName);
        if (!string.Equals(responseName, fileName, StringComparison.Ordinal))
        {
            throw new OpenClawAgentConfigurationVerificationException(
                "OpenClaw hat eine andere als die angefragte Agent-Datei zurückgegeben.");
        }

        var missing = ReadBool(file, "missing") ?? false;
        var content = missing ? null : ReadString(file, "content") ?? string.Empty;
        return new OpenClawAgentFileDto(
            agentId,
            fileName,
            missing,
            ReadLong(file, "size"),
            FromUnixMilliseconds(ReadLong(file, "updatedAtMs")),
            content,
            missing ? MissingContentHash : HashContent(content!),
            DateTimeOffset.UtcNow);
    }

    private async Task<OpenClawConfigSnapshotDto> GetConfigCoreAsync(
        CancellationToken cancellationToken)
    {
        var response = await connector.InvokeAsync(
            "config.get",
            new JsonObject(),
            cancellationToken: cancellationToken);
        return new OpenClawConfigSnapshotDto(
            ReadBool(response, "exists") ?? true,
            ReadBool(response, "valid") ?? false,
            ReadString(response, "hash"),
            SanitizePayload(response?["config"]),
            SanitizePayload(response?["issues"]),
            SanitizePayload(response?["warnings"]),
            DateTimeOffset.UtcNow);
    }

    private void EnsureAvailable(string method, string requiredScope)
    {
        if (connector.ConnectionState != GatewayConnectionState.Connected)
        {
            throw new OpenClawAgentConfigurationUnavailableException(
                "disconnected",
                method,
                requiredScope,
                "OpenClaw Gateway ist nicht verbunden.");
        }

        if (!connector.Supports(method))
        {
            throw new OpenClawAgentConfigurationUnavailableException(
                "unsupported",
                method,
                requiredScope,
                $"OpenClaw bietet {method} nicht an.");
        }

        if (!HasScope(requiredScope))
        {
            throw new OpenClawAgentConfigurationUnavailableException(
                "forbidden",
                method,
                requiredScope,
                $"OpenClaw hat den Scope {requiredScope} nicht gewährt.");
        }
    }

    private bool HasScope(string requiredScope)
    {
        var scopes = connector.GrantedScopes;
        if (scopes.Contains("operator.admin"))
            return true;
        if (scopes.Contains(requiredScope))
            return true;
        return requiredScope == "operator.read" && scopes.Contains("operator.write");
    }

    private static string NormalizeAgentId(string? agentId)
    {
        var value = agentId?.Trim();
        if (string.IsNullOrWhiteSpace(value) || value.Length > MaxAgentIdLength)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "agentId",
                $"Agent id is required and must contain at most {MaxAgentIdLength} characters.");
        }

        if (!char.IsAsciiLetterOrDigit(value[0]) ||
            value.Any(character =>
                !(char.IsAsciiLetterOrDigit(character) || character is '-' or '_')) ||
            !string.Equals(value, value.ToLowerInvariant(), StringComparison.Ordinal))
        {
            throw new OpenClawAgentConfigurationValidationException(
                "agentId",
                "Agent id may contain lowercase ASCII letters, digits, hyphens, and underscores only.");
        }

        return value;
    }

    private static string NormalizeStandardFileName(string? fileName)
    {
        var value = fileName?.Trim();
        if (string.IsNullOrWhiteSpace(value) ||
            !CanonicalFileNames.TryGetValue(value, out var canonical))
        {
            throw new OpenClawAgentConfigurationValidationException(
                "fileName",
                $"File name must be one of: {string.Join(", ", StandardFileNames)}.");
        }

        return canonical;
    }

    private static void ValidateAgentFileWrite(UpdateOpenClawAgentFileRequest? request)
    {
        if (request is null || request.Content is null)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "content",
                "Content is required.");
        }

        if (Encoding.UTF8.GetByteCount(request.Content) > MaxAgentFileBytes)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "content",
                $"Content must not exceed {MaxAgentFileBytes} UTF-8 bytes.");
        }
    }

    private static string NormalizeExpectedHash(string? hash)
        => NormalizeContentHash(hash, "expectedHash", allowMissing: true);

    private static string NormalizeContentHash(string? hash, string field, bool allowMissing)
    {
        var value = hash?.Trim();
        if (allowMissing && string.Equals(value, MissingContentHash, StringComparison.OrdinalIgnoreCase))
            return MissingContentHash;
        if (value is null ||
            value.Length != 64 ||
            value.Any(character => !Uri.IsHexDigit(character)))
        {
            throw new OpenClawAgentConfigurationValidationException(
                field,
                allowMissing
                    ? $"{field} must be a SHA-256 hash or '{MissingContentHash}'."
                    : $"{field} must be a SHA-256 hash.");
        }

        return value.ToLowerInvariant();
    }

    private static string NormalizeWorkspacePath(string? path, bool allowEmpty)
    {
        if (!TryNormalizeWorkspacePath(path, allowEmpty, out var normalized))
        {
            throw new OpenClawAgentConfigurationValidationException(
                "path",
                "Workspace path must be a safe, workspace-relative path.");
        }

        return normalized!;
    }

    private static bool TryNormalizeWorkspacePath(
        string? path,
        bool allowEmpty,
        out string? normalized)
    {
        normalized = null;
        var value = path?.Trim() ?? string.Empty;
        if (value.Length == 0)
        {
            if (allowEmpty)
                normalized = string.Empty;
            return allowEmpty;
        }

        if (value.Length > MaxWorkspacePathLength ||
            value.StartsWith('/') ||
            value.StartsWith('\\') ||
            value.StartsWith('~') ||
            value.Contains('\\') ||
            value.Contains('\0') ||
            value.Contains("//", StringComparison.Ordinal) ||
            value.Any(char.IsControl))
        {
            return false;
        }

        var segments = value.Split('/');
        if (segments.Any(segment =>
                string.IsNullOrWhiteSpace(segment) ||
                segment is "." or ".." ||
                segment.StartsWith('.') ||
                segment.Contains(':') ||
                IsSensitiveWorkspaceName(segment)))
        {
            return false;
        }

        normalized = string.Join('/', segments);
        return true;
    }

    private static bool IsSensitiveWorkspaceName(string value)
    {
        var normalized = value.ToLowerInvariant();
        if (normalized is ".env" or "id_rsa" or "id_ed25519" or "credentials.json" ||
            normalized.EndsWith(".pem", StringComparison.Ordinal) ||
            normalized.EndsWith(".key", StringComparison.Ordinal) ||
            normalized.EndsWith(".p12", StringComparison.Ordinal) ||
            normalized.EndsWith(".pfx", StringComparison.Ordinal))
        {
            return true;
        }

        return SensitiveWorkspaceNameFragments.Any(fragment =>
            normalized.Contains(fragment, StringComparison.Ordinal));
    }

    private static OpenClawWorkspaceEntryDto? MapWorkspaceEntry(JsonNode? entry)
    {
        var path = ReadString(entry, "path");
        var name = ReadString(entry, "name");
        var kind = ReadString(entry, "kind");
        if (!TryNormalizeWorkspacePath(path, allowEmpty: false, out var safePath) ||
            string.IsNullOrWhiteSpace(name) ||
            IsSensitiveWorkspaceName(name) ||
            kind is not ("file" or "directory"))
        {
            return null;
        }

        return new OpenClawWorkspaceEntryDto(
            safePath!,
            name,
            kind,
            ReadLong(entry, "size"),
            FromUnixMilliseconds(ReadLong(entry, "updatedAtMs")));
    }

    private static OpenClawAgentFileSummaryDto? MapAgentFileSummary(JsonNode? file)
    {
        var rawName = ReadString(file, "name");
        if (string.IsNullOrWhiteSpace(rawName) ||
            !CanonicalFileNames.TryGetValue(rawName, out var name))
        {
            return null;
        }

        var missing = ReadBool(file, "missing") ?? false;
        var content = ReadString(file, "content");
        return new OpenClawAgentFileSummaryDto(
            name,
            missing,
            ReadLong(file, "size"),
            FromUnixMilliseconds(ReadLong(file, "updatedAtMs")),
            missing
                ? MissingContentHash
                : content is null
                    ? null
                    : HashContent(content));
    }

    private static void ValidateConfigPatch(PatchOpenClawConfigRequest? request)
    {
        if (request?.Patch is not JsonObject)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "patch",
                "Config patch must be a JSON object.");
        }

        if (request.Note?.Length is > 1000)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "note",
                "Note must contain at most 1000 characters.");
        }

        if (request.RestartDelayMs is < 0 or > 300_000)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "restartDelayMs",
                "Restart delay must be between 0 and 300000 milliseconds.");
        }

        var secretPath = FindLiteralSecretPath(request.Patch, string.Empty);
        if (secretPath is not null)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "patch",
                $"Literal secret values are not accepted by Nexus ({secretPath}); use an OpenClaw SecretRef or environment reference.");
        }
    }

    private static string? FindLiteralSecretPath(JsonNode? node, string path)
    {
        if (node is JsonObject obj)
        {
            foreach (var property in obj)
            {
                var childPath = string.IsNullOrWhiteSpace(path)
                    ? property.Key
                    : $"{path}.{property.Key}";
                if (IsSensitiveConfigKey(property.Key) &&
                    property.Value is JsonValue value &&
                    value.TryGetValue<string>(out var secretValue) &&
                    !IsSafeSecretReference(secretValue))
                {
                    return childPath;
                }

                var nested = FindLiteralSecretPath(property.Value, childPath);
                if (nested is not null)
                    return nested;
            }
        }
        else if (node is JsonArray array)
        {
            for (var index = 0; index < array.Count; index++)
            {
                var nested = FindLiteralSecretPath(array[index], $"{path}[{index}]");
                if (nested is not null)
                    return nested;
            }
        }

        return null;
    }

    private static bool IsSensitiveConfigKey(string key)
    {
        var normalized = key.ToLowerInvariant();
        if (normalized is "inputtokens" or "outputtokens" or "totaltokens" or "contexttokens" or "maxtokens")
            return false;
        return SensitiveConfigKeyFragments.Any(fragment =>
            normalized.Contains(fragment, StringComparison.Ordinal));
    }

    private static bool IsSafeSecretReference(string value)
    {
        var trimmed = value.Trim();
        return trimmed == "__OPENCLAW_REDACTED__" ||
               (trimmed.StartsWith("${", StringComparison.Ordinal) &&
                trimmed.EndsWith('}') &&
                trimmed.Length > 3);
    }

    private static IReadOnlyList<string> NormalizeReplacePaths(IReadOnlyList<string>? replacePaths)
    {
        if (replacePaths is null)
            return [];
        if (replacePaths.Count > 256)
        {
            throw new OpenClawAgentConfigurationValidationException(
                "replacePaths",
                "replacePaths must contain at most 256 entries.");
        }

        return replacePaths
            .Select(path => NormalizeConfigPath(path, "replacePaths"))
            .Distinct(StringComparer.Ordinal)
            .ToArray();
    }

    private async Task EnsureWriteAvailableAsync(
        string method,
        CancellationToken cancellationToken)
    {
        var decision = await writeGate.EvaluateAsync(
            method,
            "operator.admin",
            cancellationToken);
        if (!decision.Allowed)
        {
            throw new OpenClawAgentConfigurationUnavailableException(
                decision.State,
                method,
                "operator.admin",
                decision.Recovery is null
                    ? decision.Message
                    : $"{decision.Message} {decision.Recovery}");
        }
    }

    private static string NormalizeConfigPath(string? path, string field)
    {
        var value = path?.Trim();
        if (string.IsNullOrWhiteSpace(value) ||
            value.Length > MaxConfigPathLength ||
            value.Any(character =>
                !(char.IsAsciiLetterOrDigit(character) ||
                  character is '_' or '.' or '/' or '[' or ']' or '-' or '*')))
        {
            throw new OpenClawAgentConfigurationValidationException(
                field,
                $"{field} must be a valid OpenClaw config path with at most {MaxConfigPathLength} characters.");
        }

        return value;
    }

    private static OpenClawAgentConfigurationConflictException BuildIdempotencyConflict(
        OpenClawOperationClaim claim)
    {
        var code = claim.Disposition switch
        {
            OpenClawOperationClaimDisposition.Conflict => "idempotency_conflict",
            OpenClawOperationClaimDisposition.InDoubt => "idempotency_in_doubt",
            _ => "idempotency_replayed_failure"
        };
        return new OpenClawAgentConfigurationConflictException(
            code,
            claim.PreviousMessage ?? "Der Idempotency-Key kann nicht erneut verwendet werden.");
    }

    private static OpenClawAgentFileWriteDto BuildAgentFileWriteResult(
        OpenClawAgentFileDto file,
        OpenClawInvocationContext invocationContext,
        string state,
        string message)
        => new(
            true,
            state,
            message,
            file,
            true,
            invocationContext.IdempotencyKey,
            invocationContext.CorrelationId,
            DateTimeOffset.UtcNow,
            OperationResultFactory.FromInvocation(
                invocationContext,
                state,
                new EntityRefDto(
                    "agent-file",
                    $"{file.AgentId}/{file.Name}",
                    file.Name),
                affectedRefs:
                [
                    new EntityRefDto("agent", file.AgentId)
                ]));

    private static OpenClawConfigPatchDto BuildConfigPatchResult(
        OpenClawConfigSnapshotDto snapshot,
        JsonNode? restart,
        OpenClawInvocationContext invocationContext,
        string state,
        string message,
        bool verified = true)
        => new(
            verified,
            state,
            message,
            snapshot,
            restart,
            true,
            invocationContext.IdempotencyKey,
            invocationContext.CorrelationId,
            DateTimeOffset.UtcNow,
            OperationResultFactory.FromInvocation(
                invocationContext,
                state,
                new EntityRefDto(
                    "config",
                    "primary",
                    "OpenClaw-Konfiguration")));

    private async Task CompleteAuditAsync(
        OpenClawInvocationContext invocationContext,
        OpenClawOperationDescriptor descriptor,
        bool ok,
        string state,
        string message,
        string? errorCode,
        CancellationToken cancellationToken)
        => await auditStore.CompleteAsync(
            invocationContext,
            descriptor,
            ok,
            state,
            message,
            errorCode,
            cancellationToken);

    private async Task TryCompleteFailedAuditAsync(
        OpenClawInvocationContext invocationContext,
        OpenClawOperationDescriptor descriptor,
        string state,
        string message)
    {
        try
        {
            await auditStore.CompleteAsync(
                invocationContext,
                descriptor,
                ok: false,
                state,
                message,
                errorCode: state,
                CancellationToken.None);
        }
        catch (Exception exception)
        {
            logger.LogWarning(
                "OpenClaw mutation audit completion failed ({ExceptionType})",
                exception.GetType().Name);
        }
    }

    private static JsonNode? SanitizePayload(JsonNode? value)
    {
        var redacted = OpenClawPayloadSanitizer.Redact(value);
        if (redacted is not null)
            RedactAbsoluteHostPaths(redacted);
        return redacted;
    }

    private static void RedactAbsoluteHostPaths(JsonNode node)
    {
        if (node is JsonObject obj)
        {
            foreach (var property in obj.ToList())
            {
                if (property.Value is JsonValue value &&
                    value.TryGetValue<string>(out var stringValue) &&
                    LooksLikeAbsoluteHostPath(stringValue))
                {
                    obj[property.Key] = "[host-path-redacted]";
                }
                else if (property.Value is not null)
                {
                    RedactAbsoluteHostPaths(property.Value);
                }
            }
        }
        else if (node is JsonArray array)
        {
            for (var index = 0; index < array.Count; index++)
            {
                var item = array[index];
                if (item is JsonValue value &&
                    value.TryGetValue<string>(out var stringValue) &&
                    LooksLikeAbsoluteHostPath(stringValue))
                {
                    array[index] = "[host-path-redacted]";
                }
                else if (item is not null)
                {
                    RedactAbsoluteHostPaths(item);
                }
            }
        }
    }

    private static bool LooksLikeAbsoluteHostPath(string value)
    {
        var trimmed = value.Trim();
        return trimmed.StartsWith("/", StringComparison.Ordinal) ||
               trimmed.StartsWith(@"\\", StringComparison.Ordinal) ||
               (trimmed.Length >= 3 &&
                char.IsAsciiLetter(trimmed[0]) &&
                trimmed[1] == ':' &&
                trimmed[2] is '\\' or '/');
    }

    private static string HashContent(string content)
        => Convert.ToHexStringLower(SHA256.HashData(Encoding.UTF8.GetBytes(content)));

    private static DateTimeOffset? FromUnixMilliseconds(long? value)
    {
        if (value is null)
            return null;
        try
        {
            return DateTimeOffset.FromUnixTimeMilliseconds(value.Value);
        }
        catch (ArgumentOutOfRangeException)
        {
            return null;
        }
    }

    private static IEnumerable<JsonNode?> ReadArray(JsonNode? node, string property)
        => node?[property] is JsonArray array
            ? array
            : [];

    private static string? ReadString(JsonNode? node, string property)
        => node?[property] is JsonValue value && value.TryGetValue<string>(out var result)
            ? result
            : null;

    private static bool? ReadBool(JsonNode? node, string property)
        => node?[property] is JsonValue value && value.TryGetValue<bool>(out var result)
            ? result
            : null;

    private static int? ReadInt(JsonNode? node, string property)
        => node?[property] is JsonValue value && value.TryGetValue<int>(out var result)
            ? result
            : null;

    private static long? ReadLong(JsonNode? node, string property)
    {
        if (node?[property] is not JsonValue value)
            return null;
        if (value.TryGetValue<long>(out var longResult))
            return longResult;
        return value.TryGetValue<int>(out var intResult) ? intResult : null;
    }
}
