using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Text;
using System.Text.Json;

namespace Zcbot.WindowsNode;

internal sealed class NodeConfigStore(NodePaths paths)
{
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("zcbot.windows-node.v1");
    private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };

    internal bool Exists => File.Exists(paths.ConfigPath);
    internal string ConfigPath => paths.ConfigPath;

    internal void Save(NodeConfig config)
    {
        Directory.CreateDirectory(paths.RootDirectory);
        RestrictDirectory(paths.RootDirectory);
        var protectedToken = ProtectedData.Protect(
            Encoding.UTF8.GetBytes(config.NodeToken), Entropy, DataProtectionScope.LocalMachine);
        var stored = new StoredNodeConfig(
            config.ServerUrl.AbsoluteUri,
            config.NodeId,
            config.InstallId,
            config.NodeName,
            Convert.ToBase64String(protectedToken),
            config.HeartbeatSeconds,
            config.Capabilities);

        var temporaryPath = paths.ConfigPath + ".tmp";
        File.WriteAllText(temporaryPath, JsonSerializer.Serialize(stored, JsonOptions), Encoding.UTF8);
        RestrictFile(temporaryPath);
        File.Move(temporaryPath, paths.ConfigPath, overwrite: false);
        RestrictFile(paths.ConfigPath);
    }

    internal NodeConfig Load()
    {
        if (!Exists)
        {
            throw new NodeConfigurationException(
                "Node is not registered. Run the enroll command first.");
        }
        try
        {
            var stored = JsonSerializer.Deserialize<StoredNodeConfig>(
                File.ReadAllText(paths.ConfigPath, Encoding.UTF8))
                ?? throw new NodeConfigurationException("Node configuration is empty.");
            var token = Encoding.UTF8.GetString(ProtectedData.Unprotect(
                Convert.FromBase64String(stored.ProtectedNodeToken),
                Entropy,
                DataProtectionScope.LocalMachine));
            if (stored.NodeId == Guid.Empty
                || stored.InstallId == Guid.Empty
                || string.IsNullOrWhiteSpace(stored.NodeName)
                || string.IsNullOrWhiteSpace(token))
            {
                throw new NodeConfigurationException("Node configuration contains an invalid identity.");
            }
            return new NodeConfig(
                NodeUri.NormalizeServerUrl(stored.ServerUrl),
                stored.NodeId,
                stored.InstallId,
                stored.NodeName,
                token,
                Math.Clamp(stored.HeartbeatSeconds, 5, 300),
                stored.Capabilities);
        }
        catch (NodeConfigurationException)
        {
            throw;
        }
        catch (Exception exception) when (
            exception is CryptographicException
                or FormatException
                or IOException
                or JsonException
                or UnauthorizedAccessException)
        {
            throw new NodeConfigurationException(
                $"Node configuration cannot be loaded: {exception.Message}");
        }
    }

    internal void DeleteLocalIdentity()
    {
        if (File.Exists(paths.ConfigPath))
        {
            File.Delete(paths.ConfigPath);
        }
        var temporaryPath = paths.ConfigPath + ".tmp";
        if (File.Exists(temporaryPath))
        {
            File.Delete(temporaryPath);
        }
    }

    internal static void RestrictDirectory(string path)
    {
        var identity = WindowsIdentity.GetCurrent();
        var user = identity.User
            ?? throw new NodeConfigurationException("Current Windows account has no security identifier.");
        var security = new DirectorySecurity();
        security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
        security.AddAccessRule(new FileSystemAccessRule(
            user, FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
            PropagationFlags.None, AccessControlType.Allow));
        security.AddAccessRule(new FileSystemAccessRule(
            new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
            FileSystemRights.FullControl, InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
            PropagationFlags.None, AccessControlType.Allow));
        new DirectoryInfo(path).SetAccessControl(security);
    }

    private static void RestrictFile(string path)
    {
        var identity = WindowsIdentity.GetCurrent();
        var user = identity.User
            ?? throw new NodeConfigurationException("Current Windows account has no security identifier.");
        var security = new FileSecurity();
        security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false);
        security.AddAccessRule(new FileSystemAccessRule(user, FileSystemRights.FullControl, AccessControlType.Allow));
        security.AddAccessRule(new FileSystemAccessRule(
            new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
            FileSystemRights.FullControl, AccessControlType.Allow));
        new FileInfo(path).SetAccessControl(security);
    }
}
