using System.Security.Cryptography;
using System.Text;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Nexus.Api.Data;
using Nexus.Api.Models;

namespace Nexus.Api.Services;

public sealed record OpenClawWriteGateDecision(
    bool Allowed,
    string State,
    string Message,
    string? Recovery = null)
{
    public static OpenClawWriteGateDecision Permit()
        => new(true, "available", "OpenClaw write boundary verified.");

    public static OpenClawWriteGateDecision Block(
        string state,
        string message,
        string? recovery = null)
        => new(false, state, message, recovery);
}

public interface IOpenClawWriteGate
{
    Task<OpenClawWriteGateDecision> EvaluateAsync(
        string method,
        string requiredScope = "operator.admin",
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Binds every OpenClaw write to the one adopted primary profile. This is a
/// local policy boundary in addition to OpenClaw's own scope enforcement.
/// </summary>
public sealed class OpenClawWriteGate(
    IServiceScopeFactory scopeFactory,
    IGatewayConnector connector,
    IOptions<GatewayConnectorOptions> gatewayOptions,
    IConfiguration configuration) : IOpenClawWriteGate
{
    public async Task<OpenClawWriteGateDecision> EvaluateAsync(
        string method,
        string requiredScope = "operator.admin",
        CancellationToken cancellationToken = default)
    {
        if (!configuration.GetValue(
                "OpenClawSetup:ExternalClientIdentitySupported",
                false)
            || !gatewayOptions.Value.ExternalClientIdentitySupported)
        {
            return OpenClawWriteGateDecision.Block(
                "experimental_blocked",
                "OpenClaw has not declared the external Nexus client identity supported.",
                "Keep writes disabled until the pinned OpenClaw release registers the Nexus client id.");
        }

        try
        {
            OpenClawGatewayProtocol.ValidateExternalClientIdentity(
                gatewayOptions.Value);
        }
        catch (OpenClawGatewayRpcException exception)
        {
            return OpenClawWriteGateDecision.Block(
                NormalizeCode(exception.Code),
                exception.Message,
                "Use only an officially registered external Nexus operator identity.");
        }

        await using var scope = scopeFactory.CreateAsyncScope();
        var db = scope.ServiceProvider.GetRequiredService<NexusDbContext>();
        var profile = await db.OpenClawConnectionProfiles
            .AsNoTracking()
            .SingleOrDefaultAsync(
                item => item.ProfileId ==
                        OpenClawConnectionProfile.PrimaryProfileId,
                cancellationToken);

        if (profile is null
            || profile.AdoptionState != OpenClawAdoptionStates.Adopted
            || !profile.ManagementEnabled)
        {
            return OpenClawWriteGateDecision.Block(
                "management_disabled",
                "The adopted primary OpenClaw profile is not approved for management.",
                "An owner must adopt, verify and explicitly enable management for the primary profile.");
        }

        if (connector.ConnectionState != GatewayConnectionState.Connected)
        {
            return OpenClawWriteGateDecision.Block(
                "gateway_unavailable",
                "OpenClaw Gateway is not connected.",
                "Restore the verified primary Gateway connection and retry.");
        }

        if (!EndpointsEqual(profile.Endpoint, connector.ActiveEndpoint))
        {
            return OpenClawWriteGateDecision.Block(
                "endpoint_trust_mismatch",
                "The connected OpenClaw endpoint is not the adopted primary endpoint.",
                "Reconnect and re-verify the adopted primary profile before enabling writes.");
        }

        var tlsPinRequired = RequiresTlsPin(profile.Endpoint);
        if (tlsPinRequired
            && (string.IsNullOrWhiteSpace(profile.TlsCertificateFingerprint)
                || string.IsNullOrWhiteSpace(
                    connector.ActiveTlsFingerprint)))
        {
            return OpenClawWriteGateDecision.Block(
                "tls_trust_missing",
                "The adopted WSS OpenClaw endpoint has no complete TLS fingerprint binding.",
                "Probe the endpoint, confirm its certificate fingerprint and re-adopt the connection.");
        }

        if ((tlsPinRequired
                || !string.IsNullOrWhiteSpace(
                    profile.TlsCertificateFingerprint)
                || !string.IsNullOrWhiteSpace(
                    connector.ActiveTlsFingerprint))
            && !FingerprintsEqual(
                profile.TlsCertificateFingerprint,
                connector.ActiveTlsFingerprint))
        {
            return OpenClawWriteGateDecision.Block(
                "tls_trust_mismatch",
                "The active OpenClaw TLS fingerprint differs from the adopted primary profile.",
                "Confirm the certificate fingerprint and re-adopt the connection.");
        }

        if (!BoundIdentityEquals(profile.DeviceId, connector.DeviceId))
        {
            return OpenClawWriteGateDecision.Block(
                "device_trust_mismatch",
                "The active OpenClaw device identity differs from the adopted primary profile.",
                "Pair and verify the expected Nexus device before enabling writes.");
        }

        if (!string.IsNullOrWhiteSpace(profile.RequiredVersion)
            && !string.Equals(
                profile.RequiredVersion,
                connector.GatewayVersion,
                StringComparison.OrdinalIgnoreCase))
        {
            return OpenClawWriteGateDecision.Block(
                "version_mismatch",
                "The connected OpenClaw version differs from the adopted primary profile.",
                "Restore the pinned OpenClaw version and re-verify the connection.");
        }

        var capabilityHash = BuildCapabilityHash(connector);
        if (string.IsNullOrWhiteSpace(profile.CapabilityHash)
            || !string.Equals(
                profile.CapabilityHash,
                capabilityHash,
                StringComparison.OrdinalIgnoreCase))
        {
            return OpenClawWriteGateDecision.Block(
                "capability_drift",
                "OpenClaw capabilities changed after management approval.",
                "Re-verify the primary profile and approve its current capability set.");
        }

        if (!connector.Supports(method))
        {
            return OpenClawWriteGateDecision.Block(
                "capability_missing",
                $"OpenClaw does not advertise required method '{method}'.",
                "Use a compatible OpenClaw release and re-verify capabilities.");
        }

        if (!string.IsNullOrWhiteSpace(requiredScope)
            && !connector.GrantedScopes.Contains(requiredScope))
        {
            return OpenClawWriteGateDecision.Block(
                "scope_upgrade_required",
                $"OpenClaw did not grant {requiredScope}.",
                "Approve the explicit scope upgrade, reconnect and re-verify the primary profile.");
        }

        return OpenClawWriteGateDecision.Permit();
    }

    public static string BuildCapabilityHash(IGatewayConnector gateway)
    {
        var contract = string.Join(
            "\n",
            new[]
            {
                gateway.GatewayVersion ?? string.Empty,
                gateway.RequiredVersion ?? string.Empty,
                gateway.ProtocolVersion?.ToString() ?? string.Empty,
                string.Join(",", gateway.GrantedScopes.Order(StringComparer.Ordinal)),
                string.Join(",", gateway.AdvertisedMethods.Order(StringComparer.Ordinal)),
                string.Join(",", gateway.AdvertisedEvents.Order(StringComparer.Ordinal))
            });
        return Convert.ToHexString(
            SHA256.HashData(Encoding.UTF8.GetBytes(contract)));
    }

    private static bool EndpointsEqual(string? expected, string? actual)
    {
        if (!Uri.TryCreate(expected, UriKind.Absolute, out var expectedUri)
            || !Uri.TryCreate(actual, UriKind.Absolute, out var actualUri))
        {
            return false;
        }

        return string.Equals(
            NormalizeEndpoint(expectedUri),
            NormalizeEndpoint(actualUri),
            StringComparison.OrdinalIgnoreCase);
    }

    private static string NormalizeEndpoint(Uri value)
    {
        var builder = new UriBuilder(value)
        {
            Host = value.Host.ToLowerInvariant(),
            Path = string.IsNullOrWhiteSpace(value.AbsolutePath)
                ? "/"
                : value.AbsolutePath.TrimEnd('/') + "/",
            Query = string.Empty,
            Fragment = string.Empty
        };
        return builder.Uri.AbsoluteUri.TrimEnd('/');
    }

    private static bool FingerprintsEqual(string? expected, string? actual)
        => string.Equals(
            NormalizeFingerprint(expected),
            NormalizeFingerprint(actual),
            StringComparison.Ordinal);

    private static bool RequiresTlsPin(string? endpoint)
        => Uri.TryCreate(endpoint, UriKind.Absolute, out var uri)
           && string.Equals(
               uri.Scheme,
               Uri.UriSchemeWss,
               StringComparison.OrdinalIgnoreCase);

    private static string NormalizeFingerprint(string? value)
        => string.IsNullOrWhiteSpace(value)
            ? string.Empty
            : new string(value
                .Where(Uri.IsHexDigit)
                .Select(char.ToUpperInvariant)
                .ToArray());

    private static bool BoundIdentityEquals(string? expected, string? actual)
        => string.Equals(
            expected?.Trim() ?? string.Empty,
            actual?.Trim() ?? string.Empty,
            StringComparison.Ordinal);

    private static string NormalizeCode(string value)
        => string.IsNullOrWhiteSpace(value)
            ? "external_identity_invalid"
            : value.Trim().ToLowerInvariant().Replace('-', '_');
}
