using System.Diagnostics.CodeAnalysis;
using System.Threading;
using StellaOps.Scanner.Core.Contracts;
using StellaOps.Scanner.Core.Utility;

namespace StellaOps.Scanner.Core.Observability;

public readonly record struct ScannerCorrelationContext(
    ScanJobId JobId,
    string CorrelationId,
    string? Stage,
    string? Component,
    string? Audience = null)
{
    public static ScannerCorrelationContext Create(
        ScanJobId jobId,
        string? stage = null,
        string? component = null,
        string? audience = null)
    {
        var correlationId = ScannerIdentifiers.CreateCorrelationId(jobId, stage, component);
        return new ScannerCorrelationContext(jobId, correlationId, stage, component, audience);
    }

    public string DeterministicHash()
        => ScannerIdentifiers.CreateDeterministicHash(
            JobId.ToString(),
            Stage ?? string.Empty,
            Component ?? string.Empty,
            Audience ?? string.Empty);
}

public static class ScannerCorrelationContextAccessor
{
    private static readonly AsyncLocal<ScannerCorrelationContext?> CurrentContext = new();

    public static ScannerCorrelationContext? Current => CurrentContext.Value;

    public static IDisposable Push(in ScannerCorrelationContext context)
    {
        var previous = CurrentContext.Value;
        CurrentContext.Value = context;
        return new DisposableScope(() => CurrentContext.Value = previous);
    }

    public static bool TryGetCorrelationId([NotNullWhen(true)] out string? correlationId)
    {
        var context = CurrentContext.Value;
        if (context.HasValue)
        {
            correlationId = context.Value.CorrelationId;
            return true;
        }

        correlationId = null;
        return false;
    }

    private sealed class DisposableScope : IDisposable
    {
        private readonly Action release;
        private bool disposed;

        public DisposableScope(Action release)
        {
            this.release = release ?? throw new ArgumentNullException(nameof(release));
        }

        public void Dispose()
        {
            if (disposed)
            {
                return;
            }

            disposed = true;
            release();
        }
    }
}
