using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.Admin.Services;

/// <summary>
///     Draft-aware write surface over <see cref="NodeAcl"/>. Replaces direct
///     <see cref="NodeAclService"/> CRUD for Admin UI grant authoring; the raw service stays
///     as the read / delete surface. Enforces the invariants listed in Phase 6.2 Stream D.2:
///     scope-uniqueness per (LdapGroup, ScopeKind, ScopeId, GenerationId), grant shape
///     consistency, and no empty permission masks.
/// </summary>
/// <remarks>
///     <para>Per decision #129 grants are additive — <see cref="NodePermissions.None"/> is
///     rejected at write time. Explicit Deny is v2.1 and is not representable in the current
///     <c>NodeAcl</c> row; attempts to express it (e.g. empty permission set) surface as
///     <see cref="InvalidNodeAclGrantException"/>.</para>
///
///     <para>Draft scope: writes always target an unpublished (Draft-state) generation id.
///     Once a generation publishes, its rows are frozen.</para>
/// </remarks>
public sealed class ValidatedNodeAclAuthoringService(OtOpcUaConfigDbContext db)
{
    /// <summary>Add a new grant row to the given draft generation.</summary>
    public async Task<NodeAcl> GrantAsync(
        long draftGenerationId,
        string clusterId,
        string ldapGroup,
        NodeAclScopeKind scopeKind,
        string? scopeId,
        NodePermissions permissions,
        string? notes,
        CancellationToken cancellationToken)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
        ArgumentException.ThrowIfNullOrWhiteSpace(ldapGroup);

        ValidateGrantShape(scopeKind, scopeId, permissions);
        await EnsureNoDuplicate(draftGenerationId, clusterId, ldapGroup, scopeKind, scopeId, cancellationToken).ConfigureAwait(false);

        var row = new NodeAcl
        {
            GenerationId = draftGenerationId,
            NodeAclId = $"acl-{Guid.NewGuid():N}"[..20],
            ClusterId = clusterId,
            LdapGroup = ldapGroup,
            ScopeKind = scopeKind,
            ScopeId = scopeId,
            PermissionFlags = permissions,
            Notes = notes,
        };
        db.NodeAcls.Add(row);
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return row;
    }

    /// <summary>
    ///     Replace an existing grant's permission set in place. Validates the new shape;
    ///     rejects attempts to blank-out to None (that's a Revoke via <see cref="NodeAclService"/>).
    /// </summary>
    public async Task<NodeAcl> UpdatePermissionsAsync(
        Guid nodeAclRowId,
        NodePermissions newPermissions,
        string? notes,
        CancellationToken cancellationToken)
    {
        if (newPermissions == NodePermissions.None)
            throw new InvalidNodeAclGrantException(
                "Permission set cannot be None — revoke the row instead of writing an empty grant.");

        var row = await db.NodeAcls.FirstOrDefaultAsync(a => a.NodeAclRowId == nodeAclRowId, cancellationToken).ConfigureAwait(false)
                  ?? throw new InvalidNodeAclGrantException($"NodeAcl row {nodeAclRowId} not found.");

        row.PermissionFlags = newPermissions;
        if (notes is not null) row.Notes = notes;
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return row;
    }

    private static void ValidateGrantShape(NodeAclScopeKind scopeKind, string? scopeId, NodePermissions permissions)
    {
        if (permissions == NodePermissions.None)
            throw new InvalidNodeAclGrantException(
                "Permission set cannot be None — grants must carry at least one flag (decision #129, additive only).");

        if (scopeKind == NodeAclScopeKind.Cluster && !string.IsNullOrEmpty(scopeId))
            throw new InvalidNodeAclGrantException(
                "Cluster-scope grants must have null ScopeId. ScopeId only applies to sub-cluster scopes.");

        if (scopeKind != NodeAclScopeKind.Cluster && string.IsNullOrEmpty(scopeId))
            throw new InvalidNodeAclGrantException(
                $"ScopeKind={scopeKind} requires a populated ScopeId.");
    }

    private async Task EnsureNoDuplicate(
        long generationId, string clusterId, string ldapGroup, NodeAclScopeKind scopeKind, string? scopeId,
        CancellationToken cancellationToken)
    {
        var exists = await db.NodeAcls.AsNoTracking()
            .AnyAsync(a => a.GenerationId == generationId
                        && a.ClusterId == clusterId
                        && a.LdapGroup == ldapGroup
                        && a.ScopeKind == scopeKind
                        && a.ScopeId == scopeId,
                cancellationToken).ConfigureAwait(false);

        if (exists)
            throw new InvalidNodeAclGrantException(
                $"A grant for (LdapGroup={ldapGroup}, ScopeKind={scopeKind}, ScopeId={scopeId}) already exists in generation {generationId}. " +
                "Update the existing row's permissions instead of inserting a duplicate.");
    }
}

/// <summary>Thrown when a <see cref="NodeAcl"/> grant authoring request violates an invariant.</summary>
public sealed class InvalidNodeAclGrantException(string message) : Exception(message);
