using System.Text.Json;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.GalaxyRepository;
using ZB.MOM.WW.GalaxyRepository.Grpc;
using ZB.MOM.WW.MxGateway.Server.Security.Audit;
using ZB.MOM.WW.MxGateway.Server.Security.Authentication;
using ZB.MOM.WW.MxGateway.Server.Sessions;

// The gateway carries its own constraint-bearing identity downstream; the shared library also
// defines an ApiKeyIdentity (scopes + opaque constraints JSON), so disambiguate to the gateway type.
using ApiKeyIdentity = ZB.MOM.WW.MxGateway.Server.Security.Authentication.ApiKeyIdentity;

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

public sealed class ConstraintEnforcer(
    IGalaxyHierarchyCache cache,
    IAuditWriter auditWriter) : IConstraintEnforcer
{
    /// <inheritdoc />
    public bool HasReadConstraints(ApiKeyIdentity? identity) =>
        identity?.EffectiveConstraints.HasReadConstraints ?? false;

    /// <inheritdoc />
    public bool HasWriteConstraints(ApiKeyIdentity? identity) =>
        identity?.EffectiveConstraints.HasWriteConstraints ?? false;

    /// <inheritdoc />
    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));
    }

    /// <inheritdoc />
    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));
    }

    /// <inheritdoc />
    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);
    }

    /// <inheritdoc />
    public async Task RecordDenialAsync(
        ApiKeyIdentity? identity,
        string commandKind,
        string target,
        ConstraintFailure failure,
        string? correlationId,
        CancellationToken cancellationToken)
    {
        // Emit a canonical Denied AuditEvent directly through the best-effort IAuditWriter:
        // structured Target ("<commandKind>:<target>") and a richer DetailsJson
        // envelope carrying constraint/message/commandKind/target.
        AuditEvent auditEvent = new()
        {
            EventId = Guid.NewGuid(),
            OccurredAtUtc = DateTimeOffset.UtcNow,
            Actor = identity?.KeyId ?? "anonymous",
            Action = "constraint-denied",
            Outcome = AuditOutcome.Denied,
            Category = CanonicalForwardingApiKeyAuditStore.ApiKeyCategory,
            Target = $"{commandKind}:{target}",
            SourceNode = null,
            CorrelationId = Guid.TryParse(correlationId, out var cid) ? cid : (Guid?)null,
            DetailsJson = JsonSerializer.Serialize(new Dictionary<string, string>
            {
                ["constraint"] = failure.ConstraintName,
                ["message"] = failure.Message,
                ["commandKind"] = commandKind,
                ["target"] = target,
                // Always preserve the raw client correlation id here so it is never silently
                // lost: the typed CorrelationId column only retains GUID-parseable ids, but
                // clients (Rust/Python/Java) commonly send non-GUID or empty trace ids. The
                // raw id is a client trace id, not a secret, so storing it is fine.
                ["clientCorrelationId"] = correlationId ?? "",
            }),
        };

        await auditWriter.WriteAsync(auditEvent, 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)
    {
        if (string.IsNullOrWhiteSpace(tagAddress))
        {
            return null;
        }

        IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress = cache.Current.Index.TagsByAddress;
        if (tagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup))
        {
            return lookup;
        }

        // Galaxy SQL keys array attributes by their suffixed FullTagReference (e.g. "Obj.Arr[]"),
        // but callers pass the bare address ("Obj.Arr") before the worker-boundary normalization
        // runs. Probe the suffixed form so a bare array name resolves to its array attribute,
        // consistent with ArrayAddressNormalizer. Only build the suffixed string on a direct miss
        // when the address is not already suffixed, and only accept it when it is truly an array.
        if (!tagAddress.EndsWith("[]", StringComparison.Ordinal)
            && tagsByAddress.TryGetValue(tagAddress + "[]", out GalaxyTagLookup? arrayLookup)
            && arrayLookup.Attribute?.IsArray == true)
        {
            return arrayLookup;
        }

        return 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;
        }

        // Plain index loops rather than Any(lambda): this runs once per item of every bulk
        // read/write, and the closures the lambdas capture (containedPath / tagAddress) allocate a
        // display class plus a delegate per call. Same short-circuit order, same result.
        for (int i = 0; i < subtreeGlobs.Count; i++)
        {
            if (GalaxyGlobMatcher.IsMatch(containedPath, subtreeGlobs[i]))
            {
                return true;
            }
        }

        for (int i = 0; i < tagGlobs.Count; i++)
        {
            if (GalaxyGlobMatcher.IsMatch(tagAddress, tagGlobs[i]))
            {
                return true;
            }
        }

        return false;
    }
}
