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

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

/// <summary>
///     EF Core implementation of <see cref="ILdapGroupRoleMappingService"/>. Enforces the
///     "exactly one of (ClusterId, IsSystemWide)" invariant at the write surface so a
///     malformed row can't land in the DB.
/// </summary>
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
{
    /// <summary>Gets LDAP group role mappings for the specified groups.</summary>
    /// <param name="ldapGroups">The LDAP group names to query.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The matching role mappings.</returns>
    public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
        IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(ldapGroups);
        var groupSet = ldapGroups.ToList();
        if (groupSet.Count == 0) return [];

        return await db.LdapGroupRoleMappings
            .AsNoTracking()
            .Where(m => groupSet.Contains(m.LdapGroup))
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);
    }

    /// <summary>Lists all LDAP group role mappings.</summary>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>All role mappings ordered by group and cluster ID.</returns>
    public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
        => await db.LdapGroupRoleMappings
            .AsNoTracking()
            .OrderBy(m => m.LdapGroup)
            .ThenBy(m => m.ClusterId)
            .ToListAsync(cancellationToken)
            .ConfigureAwait(false);

    /// <summary>Creates a new LDAP group role mapping.</summary>
    /// <param name="row">The mapping to create.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>The created mapping with generated ID and timestamp.</returns>
    public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(row);
        ValidateInvariants(row);

        if (row.Id == Guid.Empty) row.Id = Guid.NewGuid();
        if (row.CreatedAtUtc == default) row.CreatedAtUtc = DateTime.UtcNow;

        db.LdapGroupRoleMappings.Add(row);
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
        return row;
    }

    /// <summary>Deletes an LDAP group role mapping.</summary>
    /// <param name="id">The mapping identifier.</param>
    /// <param name="cancellationToken">The cancellation token.</param>
    /// <returns>A task that completes when the deletion is done.</returns>
    public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
    {
        var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
        if (existing is null) return;
        db.LdapGroupRoleMappings.Remove(existing);
        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
    }

    private static void ValidateInvariants(LdapGroupRoleMapping row)
    {
        if (string.IsNullOrWhiteSpace(row.LdapGroup))
            throw new InvalidLdapGroupRoleMappingException("LdapGroup must not be empty.");

        if (row.IsSystemWide && !string.IsNullOrEmpty(row.ClusterId))
            throw new InvalidLdapGroupRoleMappingException(
                "IsSystemWide=true requires ClusterId to be null. A fleet-wide grant cannot also be cluster-scoped.");

        if (!row.IsSystemWide && string.IsNullOrEmpty(row.ClusterId))
            throw new InvalidLdapGroupRoleMappingException(
                "IsSystemWide=false requires a populated ClusterId. A cluster-scoped grant needs its target cluster.");
    }
}
