using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using NSec.Cryptography;

namespace Nexus.Api.Services;

public sealed class OpenClawDeviceIdentity
{
    private readonly byte[] _privateKeyBlob;

    internal OpenClawDeviceIdentity(
        string deviceId,
        string publicKey,
        byte[] privateKeyBlob)
    {
        DeviceId = deviceId;
        PublicKey = publicKey;
        _privateKeyBlob = privateKeyBlob.ToArray();
    }

    public string DeviceId { get; }
    public string PublicKey { get; }

    public string Sign(string payload)
    {
        ArgumentNullException.ThrowIfNull(payload);
        using var key = Key.Import(
            SignatureAlgorithm.Ed25519,
            _privateKeyBlob,
            KeyBlobFormat.NSecPrivateKey);
        var signature = SignatureAlgorithm.Ed25519.Sign(
            key,
            Encoding.UTF8.GetBytes(payload));
        return Base64UrlEncode(signature);
    }

    public bool Verify(string payload, string signature)
    {
        try
        {
            var publicKey = NSec.Cryptography.PublicKey.Import(
                SignatureAlgorithm.Ed25519,
                Base64UrlDecode(PublicKey),
                KeyBlobFormat.RawPublicKey);
            return SignatureAlgorithm.Ed25519.Verify(
                publicKey,
                Encoding.UTF8.GetBytes(payload),
                Base64UrlDecode(signature));
        }
        catch (FormatException)
        {
            return false;
        }
    }

    internal byte[] ExportPrivateKeyBlob() => _privateKeyBlob.ToArray();

    internal static string Base64UrlEncode(ReadOnlySpan<byte> value)
        => Convert.ToBase64String(value)
            .TrimEnd('=')
            .Replace('+', '-')
            .Replace('/', '_');

    internal static byte[] Base64UrlDecode(string value)
    {
        var normalized = value.Replace('-', '+').Replace('_', '/');
        normalized = normalized.PadRight(normalized.Length + ((4 - normalized.Length % 4) % 4), '=');
        return Convert.FromBase64String(normalized);
    }
}

public sealed record OpenClawDeviceToken(
    string Token,
    IReadOnlyList<string> Scopes,
    string Role,
    string GatewayBinding = "");

public interface IOpenClawDeviceIdentityStore
{
    string StatePath { get; }
    Task<OpenClawDeviceIdentity> LoadOrCreateAsync(CancellationToken cancellationToken = default);
    Task<OpenClawDeviceToken?> LoadTokenAsync(
        string deviceId,
        string role,
        CancellationToken cancellationToken = default);
    Task<OpenClawDeviceToken?> LoadTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        CancellationToken cancellationToken = default);
    Task StoreTokenAsync(
        string deviceId,
        string role,
        string token,
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default);
    Task StoreTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        string token,
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default);
    Task<bool> RemoveTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        CancellationToken cancellationToken = default);
}

/// <summary>
/// Persists Nexus' backend-only OpenClaw device key and device token. The file
/// is outside the repository by default, is written atomically, and is reduced
/// to owner-only permissions on Unix. Windows uses the current user's private
/// LocalApplicationData ACL inheritance unless an explicit path is configured.
/// </summary>
public sealed class OpenClawDeviceIdentityStore : IOpenClawDeviceIdentityStore
{
    private const int CurrentSchemaVersion = 2;
    private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
    {
        WriteIndented = true
    };

    private readonly ILogger<OpenClawDeviceIdentityStore> _logger;
    private readonly SemaphoreSlim _gate = new(1, 1);
    private readonly string _statePath;

    private DeviceState? _state;
    private OpenClawDeviceIdentity? _identity;

    public OpenClawDeviceIdentityStore(
        IOptions<GatewayConnectorOptions> options,
        ILogger<OpenClawDeviceIdentityStore> logger)
    {
        _logger = logger;
        _statePath = ResolveStatePath(options.Value.DeviceStatePath);
    }

    public string StatePath => _statePath;

    public async Task<OpenClawDeviceIdentity> LoadOrCreateAsync(
        CancellationToken cancellationToken = default)
    {
        await _gate.WaitAsync(cancellationToken);
        try
        {
            if (_identity is not null)
                return _identity;

            if (File.Exists(_statePath))
            {
                EnsureRegularFile(_statePath);
                EnsureRestrictedPermissions(_statePath, isDirectory: false);
                _state = await ReadStateAsync(cancellationToken);
                _identity = ValidateAndCreateIdentity(_state);
                return _identity;
            }

            var directory = Path.GetDirectoryName(_statePath)
                ?? throw new InvalidOperationException("OpenClaw device state path has no parent directory.");
            Directory.CreateDirectory(directory);
            EnsureRestrictedPermissions(directory, isDirectory: true);

            using var key = Key.Create(
                SignatureAlgorithm.Ed25519,
                new KeyCreationParameters
                {
                    ExportPolicy = KeyExportPolicies.AllowPlaintextExport
                });
            var privateKey = key.Export(KeyBlobFormat.NSecPrivateKey);
            var publicKey = key.PublicKey.Export(KeyBlobFormat.RawPublicKey);
            var publicKeyEncoded = OpenClawDeviceIdentity.Base64UrlEncode(publicKey);
            var deviceId = Convert.ToHexStringLower(SHA256.HashData(publicKey));

            _state = new DeviceState
            {
                SchemaVersion = CurrentSchemaVersion,
                DeviceId = deviceId,
                PublicKey = publicKeyEncoded,
                PrivateKey = OpenClawDeviceIdentity.Base64UrlEncode(privateKey),
                Tokens = []
            };
            await WriteStateAsync(_state, cancellationToken);
            _identity = new OpenClawDeviceIdentity(deviceId, publicKeyEncoded, privateKey);

            _logger.LogInformation(
                "Created persistent Nexus OpenClaw backend device identity {DeviceId} at {StatePath}",
                deviceId,
                _statePath);
            return _identity;
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<OpenClawDeviceToken?> LoadTokenAsync(
        string deviceId,
        string role,
        CancellationToken cancellationToken = default)
        => await LoadTokenAsync(
            deviceId,
            role,
            gatewayBinding: string.Empty,
            cancellationToken);

    public async Task<OpenClawDeviceToken?> LoadTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(deviceId) || string.IsNullOrWhiteSpace(role))
            return null;

        await _gate.WaitAsync(cancellationToken);
        try
        {
            if (_state is null)
            {
                if (!File.Exists(_statePath))
                    return null;
                _state = await ReadStateAsync(cancellationToken);
                _identity = ValidateAndCreateIdentity(_state);
            }

            if (!string.Equals(_state.DeviceId, deviceId, StringComparison.Ordinal))
                throw new InvalidOperationException("OpenClaw device state id does not match the active identity.");

            var token = _state.Tokens.FirstOrDefault(item =>
                string.Equals(item.Role, role, StringComparison.Ordinal) &&
                string.Equals(item.GatewayBinding, gatewayBinding, StringComparison.Ordinal));
            return token is null || string.IsNullOrWhiteSpace(token.Token)
                ? null
                : new OpenClawDeviceToken(
                    token.Token,
                    token.Scopes
                        .Where(scope => !string.IsNullOrWhiteSpace(scope))
                        .Distinct(StringComparer.Ordinal)
                        .ToArray(),
                    token.Role,
                    token.GatewayBinding ?? string.Empty);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task StoreTokenAsync(
        string deviceId,
        string role,
        string token,
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default)
        => await StoreTokenAsync(
            deviceId,
            role,
            gatewayBinding: string.Empty,
            token,
            scopes,
            cancellationToken);

    public async Task StoreTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        string token,
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(token))
            throw new ArgumentException("OpenClaw device token is required.", nameof(token));

        await _gate.WaitAsync(cancellationToken);
        try
        {
            if (_state is null)
            {
                if (!File.Exists(_statePath))
                    throw new InvalidOperationException("OpenClaw device identity must exist before storing its token.");
                _state = await ReadStateAsync(cancellationToken);
                _identity = ValidateAndCreateIdentity(_state);
            }

            if (!string.Equals(_state.DeviceId, deviceId, StringComparison.Ordinal))
                throw new InvalidOperationException("Refusing to store a token for a different OpenClaw device.");

            _state.Tokens.RemoveAll(item =>
                string.Equals(item.Role, role, StringComparison.Ordinal) &&
                string.Equals(item.GatewayBinding, gatewayBinding, StringComparison.Ordinal));
            _state.Tokens.Add(new DeviceTokenState
            {
                Role = role,
                GatewayBinding = gatewayBinding,
                Token = token,
                Scopes = scopes
                    .Where(scope => !string.IsNullOrWhiteSpace(scope))
                    .Distinct(StringComparer.Ordinal)
                    .ToList()
            });
            await WriteStateAsync(_state, cancellationToken);
        }
        finally
        {
            _gate.Release();
        }
    }

    public async Task<bool> RemoveTokenAsync(
        string deviceId,
        string role,
        string gatewayBinding,
        CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrWhiteSpace(deviceId) || string.IsNullOrWhiteSpace(role))
            return false;

        await _gate.WaitAsync(cancellationToken);
        try
        {
            if (_state is null)
            {
                if (!File.Exists(_statePath))
                    return false;
                _state = await ReadStateAsync(cancellationToken);
                _identity = ValidateAndCreateIdentity(_state);
            }

            if (!string.Equals(_state.DeviceId, deviceId, StringComparison.Ordinal))
                throw new InvalidOperationException("OpenClaw device state id does not match the active identity.");

            var removed = _state.Tokens.RemoveAll(item =>
                string.Equals(item.Role, role, StringComparison.Ordinal) &&
                string.Equals(item.GatewayBinding, gatewayBinding, StringComparison.Ordinal));
            if (removed > 0)
                await WriteStateAsync(_state, cancellationToken);
            return removed > 0;
        }
        finally
        {
            _gate.Release();
        }
    }

    public static string ResolveStatePath(string? configuredPath)
    {
        if (!string.IsNullOrWhiteSpace(configuredPath))
            return Path.GetFullPath(configuredPath.Trim());

        var localData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        if (string.IsNullOrWhiteSpace(localData))
            localData = AppContext.BaseDirectory;
        return Path.Combine(localData, "Nexus", "openclaw", "device-state.json");
    }

    private async Task<DeviceState> ReadStateAsync(CancellationToken cancellationToken)
    {
        try
        {
            await using var stream = new FileStream(
                _statePath,
                FileMode.Open,
                FileAccess.Read,
                FileShare.Read,
                4096,
                FileOptions.SequentialScan);
            var state = await JsonSerializer.DeserializeAsync<DeviceState>(
                stream,
                JsonOptions,
                cancellationToken);
            return state ?? throw new InvalidDataException("OpenClaw device state is empty.");
        }
        catch (Exception exception) when (
            exception is JsonException or FormatException or InvalidDataException)
        {
            throw new InvalidDataException(
                $"OpenClaw device state at '{_statePath}' is invalid; refusing to rotate the identity silently.",
                exception);
        }
    }

    internal static void EnsureRegularFile(string path)
    {
        if ((File.GetAttributes(path) & FileAttributes.ReparsePoint) != 0)
        {
            throw new InvalidDataException(
                $"OpenClaw device state at '{path}' must be a regular file, not a symbolic link or reparse point.");
        }
    }

    private static OpenClawDeviceIdentity ValidateAndCreateIdentity(DeviceState state)
    {
        if (state.SchemaVersion is not 1 and not CurrentSchemaVersion ||
            string.IsNullOrWhiteSpace(state.DeviceId) ||
            string.IsNullOrWhiteSpace(state.PublicKey) ||
            string.IsNullOrWhiteSpace(state.PrivateKey))
        {
            throw new InvalidDataException("OpenClaw device state is incomplete or has an unsupported schema.");
        }

        var privateKey = OpenClawDeviceIdentity.Base64UrlDecode(state.PrivateKey);
        using var key = Key.Import(
            SignatureAlgorithm.Ed25519,
            privateKey,
            KeyBlobFormat.NSecPrivateKey);
        var publicKey = key.PublicKey.Export(KeyBlobFormat.RawPublicKey);
        var expectedPublicKey = OpenClawDeviceIdentity.Base64UrlEncode(publicKey);
        var expectedDeviceId = Convert.ToHexStringLower(SHA256.HashData(publicKey));

        if (!CryptographicOperations.FixedTimeEquals(
                Encoding.ASCII.GetBytes(expectedPublicKey),
                Encoding.ASCII.GetBytes(state.PublicKey)) ||
            !CryptographicOperations.FixedTimeEquals(
                Encoding.ASCII.GetBytes(expectedDeviceId),
                Encoding.ASCII.GetBytes(state.DeviceId)))
        {
            throw new InvalidDataException("OpenClaw device key, public key, and device id do not match.");
        }

        state.Tokens ??= [];
        if (state.SchemaVersion == 1)
        {
            foreach (var token in state.Tokens)
                token.GatewayBinding ??= string.Empty;
            state.SchemaVersion = CurrentSchemaVersion;
        }
        return new OpenClawDeviceIdentity(expectedDeviceId, expectedPublicKey, privateKey);
    }

    private async Task WriteStateAsync(DeviceState state, CancellationToken cancellationToken)
    {
        var directory = Path.GetDirectoryName(_statePath)
            ?? throw new InvalidOperationException("OpenClaw device state path has no parent directory.");
        Directory.CreateDirectory(directory);
        EnsureRestrictedPermissions(directory, isDirectory: true);

        var temporaryPath = Path.Combine(directory, $".{Path.GetFileName(_statePath)}.{Guid.NewGuid():N}.tmp");
        try
        {
            await using (var stream = new FileStream(
                temporaryPath,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None,
                4096,
                FileOptions.WriteThrough))
            {
                await JsonSerializer.SerializeAsync(stream, state, JsonOptions, cancellationToken);
                await stream.FlushAsync(cancellationToken);
            }

            EnsureRestrictedPermissions(temporaryPath, isDirectory: false);
            File.Move(temporaryPath, _statePath, overwrite: true);
            EnsureRestrictedPermissions(_statePath, isDirectory: false);
        }
        finally
        {
            if (File.Exists(temporaryPath))
                File.Delete(temporaryPath);
        }
    }

    internal static void EnsureRestrictedPermissions(string path, bool isDirectory)
    {
        if (OperatingSystem.IsWindows())
            return;

        var mode = isDirectory
            ? UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute
            : UnixFileMode.UserRead | UnixFileMode.UserWrite;
        File.SetUnixFileMode(path, mode);
    }

    private sealed class DeviceState
    {
        public int SchemaVersion { get; set; }
        public string DeviceId { get; set; } = string.Empty;
        public string PublicKey { get; set; } = string.Empty;
        public string PrivateKey { get; set; } = string.Empty;
        public List<DeviceTokenState> Tokens { get; set; } = [];
    }

    private sealed class DeviceTokenState
    {
        public string Role { get; set; } = "operator";
        public string? GatewayBinding { get; set; } = string.Empty;
        public string Token { get; set; } = string.Empty;
        public List<string> Scopes { get; set; } = [];
    }
}
