using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Sessions;

namespace ZB.MOM.WW.MxGateway.Server.Security.Authorization;

public sealed class ConstraintEnforcer(
    IGalaxyHierarchyCache cache,
    IApiKeyAuditStore auditStore) : IConstraintEnforcer
{
    public Task<ConstraintFailure?> CheckReadTagAsync(
        ApiKeyIdentity? identity,
        string tagAddress,
        CancellationToken cancellationToken)
    {
        ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
        if (!constraints.HasReadConstraints)
        {
            return Task.FromResult<ConstraintFailure?>(null);
        }

        return Task.FromResult(CheckReadTarget(constraints, tagAddress));
    }

    public Task<ConstraintFailure?> CheckReadHandleAsync(
        ApiKeyIdentity? identity,
        GatewaySession session,
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken)
    {
        ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
        if (!constraints.HasReadConstraints)
        {
            return Task.FromResult<ConstraintFailure?>(null);
        }

        if (!session.TryGetItemRegistration(serverHandle, itemHandle, out SessionItemRegistration registration))
        {
            return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("item_handle", "Item handle is not registered in the constrained session."));
        }

        return Task.FromResult(CheckReadTarget(constraints, registration.TagAddress));
    }

    public Task<ConstraintFailure?> CheckWriteHandleAsync(
        ApiKeyIdentity? identity,
        GatewaySession session,
        int serverHandle,
        int itemHandle,
        CancellationToken cancellationToken)
    {
        ApiKeyConstraints constraints = identity?.EffectiveConstraints ?? ApiKeyConstraints.Empty;
        if (!constraints.HasWriteConstraints)
        {
            return Task.FromResult<ConstraintFailure?>(null);
        }

        if (!session.TryGetItemRegistration(serverHandle, itemHandle, out SessionItemRegistration registration))
        {
            return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("item_handle", "Item handle is not registered in the constrained session."));
        }

        GalaxyTagLookup? target = ResolveTarget(registration.TagAddress);
        if (target is null)
        {
            return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("tag_metadata", "Tag metadata is not available in the Galaxy hierarchy cache."));
        }

        if (!MatchesPathOrTag(target.ContainedPath, registration.TagAddress, constraints.WriteSubtrees, constraints.WriteTagGlobs))
        {
            return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("write_scope", "Tag is outside the API key write scope."));
        }

        if (constraints.MaxWriteClassification is { } maxClassification)
        {
            GalaxyAttribute? attribute = target.Attribute;
            if (attribute is null)
            {
                return Task.FromResult<ConstraintFailure?>(new ConstraintFailure("max_write_classification", "Attribute security classification is not available."));
            }

            if (attribute.SecurityClassification > maxClassification)
            {
                return Task.FromResult<ConstraintFailure?>(new ConstraintFailure(
                    "max_write_classification",
                    $"Attribute security classification {attribute.SecurityClassification} exceeds allowed maximum {maxClassification}."));
            }
        }

        return Task.FromResult<ConstraintFailure?>(null);
    }

    public async Task RecordDenialAsync(
        ApiKeyIdentity? identity,
        string commandKind,
        string target,
        ConstraintFailure failure,
        CancellationToken cancellationToken)
    {
        await auditStore.AppendAsync(
                new ApiKeyAuditEntry(
                    KeyId: identity?.KeyId,
                    EventType: "constraint-denied",
                    RemoteAddress: null,
                    Details: $"{commandKind}: {target}: {failure.ConstraintName}: {failure.Message}"),
                cancellationToken)
            .ConfigureAwait(false);
    }

    private ConstraintFailure? CheckReadTarget(
        ApiKeyConstraints constraints,
        string tagAddress)
    {
        GalaxyTagLookup? target = ResolveTarget(tagAddress);
        if (target is null)
        {
            return new ConstraintFailure("tag_metadata", "Tag metadata is not available in the Galaxy hierarchy cache.");
        }

        if (!MatchesPathOrTag(target.ContainedPath, tagAddress, constraints.ReadSubtrees, constraints.ReadTagGlobs))
        {
            return new ConstraintFailure("read_scope", "Tag is outside the API key read scope.");
        }

        if (constraints.ReadAlarmOnly && target.Attribute is not { IsAlarm: true })
        {
            return new ConstraintFailure("read_alarm_only", "Tag is not an alarm-bearing attribute.");
        }

        if (constraints.ReadHistorizedOnly && target.Attribute is not { IsHistorized: true })
        {
            return new ConstraintFailure("read_historized_only", "Tag is not a historized attribute.");
        }

        return null;
    }

    private GalaxyTagLookup? ResolveTarget(string tagAddress)
    {
        GalaxyHierarchyCacheEntry entry = cache.Current;
        return !string.IsNullOrWhiteSpace(tagAddress)
            && entry.Index.TagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup)
                ? lookup
                : null;
    }

    private static bool MatchesPathOrTag(
        string containedPath,
        string tagAddress,
        IReadOnlyList<string> subtreeGlobs,
        IReadOnlyList<string> tagGlobs)
    {
        bool hasSubtreeConstraint = subtreeGlobs.Count > 0;
        bool hasTagConstraint = tagGlobs.Count > 0;
        if (!hasSubtreeConstraint && !hasTagConstraint)
        {
            return true;
        }

        return subtreeGlobs.Any(glob => GalaxyGlobMatcher.IsMatch(containedPath, glob))
            || tagGlobs.Any(glob => GalaxyGlobMatcher.IsMatch(tagAddress, glob));
    }
}
