using System.Text.Json;
using ZB.MOM.WW.Audit;
using ZB.MOM.WW.MxGateway.Contracts.Proto.Galaxy;
using ZB.MOM.WW.MxGateway.Server.Galaxy;
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
{
    /// <summary>Checks read constraints on a tag address.</summary>
    /// <param name="identity">The API key identity to check constraints for.</param>
    /// <param name="tagAddress">Tag address to validate.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    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));
    }

    /// <summary>Checks read constraints on a server and item handle.</summary>
    /// <param name="identity">The API key identity to check constraints for.</param>
    /// <param name="session">The gateway session containing handle registrations.</param>
    /// <param name="serverHandle">The MXAccess server handle.</param>
    /// <param name="itemHandle">The MXAccess item handle.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    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));
    }

    /// <summary>Checks write constraints on a server and item handle.</summary>
    /// <param name="identity">The API key identity to check constraints for.</param>
    /// <param name="session">The gateway session containing handle registrations.</param>
    /// <param name="serverHandle">The MXAccess server handle.</param>
    /// <param name="itemHandle">The MXAccess item handle.</param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    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);
    }

    /// <summary>Records a constraint denial audit entry.</summary>
    /// <param name="identity">The API key identity that was denied.</param>
    /// <param name="commandKind">The command type (e.g., read, write).</param>
    /// <param name="target">The target being accessed (tag address or handle).</param>
    /// <param name="failure">The constraint failure details.</param>
    /// <param name="correlationId">
    ///     The per-request client correlation id, if any. Persisted as the audit record's typed
    ///     <c>CorrelationId</c> when it parses as a GUID; a non-GUID value leaves that column null.
    ///     The raw string is always preserved in <c>DetailsJson["clientCorrelationId"]</c> so a
    ///     non-GUID id (e.g. from Rust/Python/Java clients) is never silently lost.
    /// </param>
    /// <param name="cancellationToken">Token to observe for cancellation.</param>
    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
        // (Task 2.3 #6): 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)
    {
        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));
    }
}
