using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Nodes;

namespace Nexus.Api.Services;

public sealed record OpenClawGatewayHello(
    int Protocol,
    string? ServerVersion,
    string? ConnectionId,
    IReadOnlySet<string> Methods,
    IReadOnlySet<string> Events,
    IReadOnlySet<string> Scopes,
    int? MaxPayload,
    int? MaxBufferedBytes,
    int? TickIntervalMs,
    string? DeviceToken,
    string? Role);

public sealed record OpenClawGatewayDeviceProof(
    string Id,
    string PublicKey,
    string Signature,
    long SignedAt,
    string Nonce);

/// <summary>
/// Small protocol-v4 boundary used by <see cref="GatewayConnector"/>.
/// Keeping frame construction and parsing here makes the integration contract
/// independently testable without a live Gateway.
/// </summary>
public static class OpenClawGatewayProtocol
{
    public const int CurrentProtocol = 4;
    public const string DefaultRequiredGatewayVersion = "2026.7.1";

    public static JsonObject BuildConnectRequest(
        string requestId,
        GatewayConnectorOptions options,
        string? token,
        string? password,
        string clientVersion,
        string platform,
        string locale,
        OpenClawGatewayDeviceProof? device = null,
        IReadOnlyList<string>? scopes = null,
        string? deviceFamily = null,
        string? deviceToken = null)
    {
        var auth = new JsonObject();
        if (!string.IsNullOrWhiteSpace(deviceToken))
        {
            auth["token"] = deviceToken;
            auth["deviceToken"] = deviceToken;
        }
        else if (!string.IsNullOrWhiteSpace(password))
            auth["password"] = password;
        else if (!string.IsNullOrWhiteSpace(token))
            auth["token"] = token;

        var client = new JsonObject
        {
            ["id"] = options.ClientId,
            ["version"] = clientVersion,
            ["platform"] = platform,
            ["mode"] = options.ClientMode,
            ["displayName"] = options.ClientDisplayName
        };
        if (!string.IsNullOrWhiteSpace(options.ClientInstanceId))
            client["instanceId"] = options.ClientInstanceId.Trim();
        if (!string.IsNullOrWhiteSpace(deviceFamily))
            client["deviceFamily"] = deviceFamily.Trim();

        var parameters = new JsonObject
        {
            ["minProtocol"] = options.ProtocolVersion,
            ["maxProtocol"] = options.ProtocolVersion,
            ["client"] = client,
            ["role"] = "operator",
            ["scopes"] = ToJsonArray(scopes ?? options.Scopes),
            ["caps"] = ToJsonArray(options.Capabilities),
            ["commands"] = new JsonArray(),
            ["permissions"] = new JsonObject(),
            ["locale"] = locale,
            ["userAgent"] = $"nexus/{clientVersion}"
        };

        if (auth.Count > 0)
            parameters["auth"] = auth;
        if (device is not null)
        {
            parameters["device"] = new JsonObject
            {
                ["id"] = device.Id,
                ["publicKey"] = device.PublicKey,
                ["signature"] = device.Signature,
                ["signedAt"] = device.SignedAt,
                ["nonce"] = device.Nonce
            };
        }

        return new JsonObject
        {
            ["type"] = "req",
            ["id"] = requestId,
            ["method"] = "connect",
            ["params"] = parameters
        };
    }

    public static JsonObject BuildRpcRequest(
        string requestId,
        string method,
        JsonNode? parameters,
        OpenClawInvocationContext? invocationContext = null)
    {
        if (string.IsNullOrWhiteSpace(requestId))
            throw new ArgumentException("Gateway request id is required.", nameof(requestId));
        if (string.IsNullOrWhiteSpace(method))
            throw new ArgumentException("Gateway method is required.", nameof(method));

        var requestParameters = parameters?.DeepClone() ?? new JsonObject();
        if (invocationContext?.IncludeIdempotencyParameter == true)
        {
            if (requestParameters is not JsonObject parameterObject)
            {
                throw new ArgumentException(
                    "Gateway idempotency can only be attached to object parameters.",
                    nameof(parameters));
            }

            if (parameterObject.TryGetPropertyValue("idempotencyKey", out var existing) &&
                !string.Equals(
                    existing?.GetValue<string>(),
                    invocationContext.IdempotencyKey,
                    StringComparison.Ordinal))
            {
                throw new ArgumentException(
                    "Gateway parameters contain a conflicting idempotencyKey.",
                    nameof(parameters));
            }

            parameterObject["idempotencyKey"] = invocationContext.IdempotencyKey;
        }

        var request = new JsonObject
        {
            ["type"] = "req",
            ["id"] = requestId,
            ["method"] = method,
            ["params"] = requestParameters
        };

        if (invocationContext is not null)
        {
            if (!IsValidTraceParent(invocationContext.TraceParent))
                throw new ArgumentException("Invocation traceparent is not a valid W3C trace context.", nameof(invocationContext));
            request["traceparent"] = invocationContext.TraceParent;
        }

        return request;
    }

    public static string BuildDeviceAuthPayloadV3(
        string deviceId,
        string clientId,
        string clientMode,
        string role,
        IEnumerable<string> scopes,
        long signedAtMs,
        string? token,
        string nonce,
        string? platform,
        string? deviceFamily)
    {
        return string.Join(
            '|',
            "v3",
            deviceId,
            clientId,
            clientMode,
            role,
            string.Join(',', scopes),
            signedAtMs.ToString(System.Globalization.CultureInfo.InvariantCulture),
            token ?? string.Empty,
            nonce,
            NormalizeDeviceMetadata(platform),
            NormalizeDeviceMetadata(deviceFamily));
    }

    public static bool IsConnectChallenge(JsonNode? frame, out string? nonce)
    {
        nonce = null;
        if (!string.Equals(frame?["type"]?.GetValue<string>(), "event", StringComparison.Ordinal) ||
            !string.Equals(frame?["event"]?.GetValue<string>(), "connect.challenge", StringComparison.Ordinal))
            return false;

        nonce = frame?["payload"]?["nonce"]?.GetValue<string>();
        return !string.IsNullOrWhiteSpace(nonce);
    }

    public static OpenClawGatewayHello ParseHello(JsonNode? frame, string requestId)
    {
        if (!string.Equals(frame?["type"]?.GetValue<string>(), "res", StringComparison.Ordinal) ||
            !string.Equals(frame?["id"]?.GetValue<string>(), requestId, StringComparison.Ordinal))
            throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "Gateway returned an unexpected connect response.");

        if (frame?["ok"]?.GetValue<bool>() != true)
            throw CreateRpcException(frame?["error"]);

        var payload = frame?["payload"];
        if (!string.Equals(payload?["type"]?.GetValue<string>(), "hello-ok", StringComparison.Ordinal))
            throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "Gateway connect response did not contain hello-ok.");

        var protocol = payload?["protocol"]?.GetValue<int>()
            ?? throw new OpenClawGatewayRpcException("INVALID_HANDSHAKE", "Gateway hello-ok omitted the protocol version.");

        return new OpenClawGatewayHello(
            protocol,
            payload?["server"]?["version"]?.GetValue<string>(),
            payload?["server"]?["connId"]?.GetValue<string>(),
            ReadStringSet(payload?["features"]?["methods"]),
            ReadStringSet(payload?["features"]?["events"]),
            ReadStringSet(payload?["auth"]?["scopes"]),
            TryGetInt(payload?["policy"]?["maxPayload"]),
            TryGetInt(payload?["policy"]?["maxBufferedBytes"]),
            TryGetInt(payload?["policy"]?["tickIntervalMs"]),
            payload?["auth"]?["deviceToken"]?.GetValue<string>(),
            payload?["auth"]?["role"]?.GetValue<string>());
    }

    public static bool IsValidTraceParent(string? value)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 128)
            return false;
        return ActivityContext.TryParse(value, null, out _);
    }

    public static bool RequiresDeviceIdentity(Uri endpoint)
    {
        ArgumentNullException.ThrowIfNull(endpoint);
        return !(endpoint.IsLoopback ||
            string.Equals(endpoint.Host, "localhost", StringComparison.OrdinalIgnoreCase));
    }

    public static void ValidateExternalClientIdentity(GatewayConnectorOptions options)
    {
        ArgumentNullException.ThrowIfNull(options);

        var clientId = options.ClientId?.Trim();
        var clientMode = options.ClientMode?.Trim();
        if (string.IsNullOrWhiteSpace(clientId) || string.IsNullOrWhiteSpace(clientMode))
        {
            throw new OpenClawGatewayRpcException(
                "UNSUPPORTED_CLIENT_ID",
                "Nexus requires an explicit OpenClaw client id and client mode.");
        }

        if (string.Equals(clientId, "gateway-client", StringComparison.Ordinal) &&
            string.Equals(clientMode, "backend", StringComparison.Ordinal) &&
            !options.AllowReservedInternalClientIdentity)
        {
            throw new OpenClawGatewayRpcException(
                "RESERVED_CLIENT_ID",
                "OpenClaw reserves gateway-client/backend for its own trusted internal helpers. Nexus will not impersonate it.");
        }

        if (!options.ExternalClientIdentitySupported)
        {
            throw new OpenClawGatewayRpcException(
                "EXTERNAL_CLIENT_ID_UNSUPPORTED",
                $"OpenClaw does not yet advertise an approved external client identity for '{clientId}'. Attach remains read-only blocked until the Gateway contract explicitly supports it.");
        }

        if (!string.Equals(clientId, "nexus", StringComparison.Ordinal))
        {
            throw new OpenClawGatewayRpcException(
                "UNSUPPORTED_CLIENT_ID",
                "Nexus will not impersonate OpenClaw's CLI, Control UI, native apps, probes, tests, or node hosts.");
        }
    }

    private static string NormalizeDeviceMetadata(string? value)
        => string.IsNullOrWhiteSpace(value)
            ? string.Empty
            : value.Trim().ToLowerInvariant();

    public static OpenClawGatewayRpcException CreateRpcException(JsonNode? error)
    {
        var code = error?["code"]?.GetValue<string>() ?? "GATEWAY_ERROR";
        var message = error?["message"]?.GetValue<string>() ?? "OpenClaw Gateway request failed.";
        var retryable = error?["retryable"]?.GetValue<bool>() ?? false;
        var retryAfterMs = TryGetInt(error?["retryAfterMs"]);
        return new OpenClawGatewayRpcException(
            code,
            message,
            error?["details"]?.DeepClone(),
            retryable,
            retryAfterMs);
    }

    public static bool TryReadPairingRequest(
        OpenClawGatewayRpcException exception,
        out string? requestId)
    {
        ArgumentNullException.ThrowIfNull(exception);
        requestId = TryGetString(exception.Details?["requestId"]);
        var detailsCode = TryGetString(exception.Details?["code"]);
        return string.Equals(exception.Code, "PAIRING_REQUIRED", StringComparison.OrdinalIgnoreCase) ||
            string.Equals(detailsCode, "PAIRING_REQUIRED", StringComparison.OrdinalIgnoreCase);
    }

    private static JsonArray ToJsonArray(IEnumerable<string> values)
    {
        var result = new JsonArray();
        foreach (var value in values.Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.Ordinal))
            result.Add(value);
        return result;
    }

    private static IReadOnlySet<string> ReadStringSet(JsonNode? node)
    {
        if (node is not JsonArray array)
            return new HashSet<string>(StringComparer.Ordinal);

        return array
            .Select(item => item?.GetValue<string>())
            .Where(item => !string.IsNullOrWhiteSpace(item))
            .Select(item => item!)
            .ToHashSet(StringComparer.Ordinal);
    }

    private static int? TryGetInt(JsonNode? node)
    {
        if (node is null)
            return null;

        try
        {
            return node.GetValueKind() switch
            {
                JsonValueKind.Number => node.GetValue<int>(),
                JsonValueKind.String when int.TryParse(node.GetValue<string>(), out var value) => value,
                _ => null
            };
        }
        catch
        {
            return null;
        }
    }

    private static string? TryGetString(JsonNode? node)
    {
        try
        {
            return node?.GetValue<string>();
        }
        catch
        {
            return null;
        }
    }
}
