using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using StellaOps.Scanner.Worker.Diagnostics;

namespace StellaOps.Scanner.Worker.Processing;

public sealed partial class ScanProgressReporter
{
    private readonly ScannerWorkerMetrics _metrics;
    private readonly ILogger<ScanProgressReporter> _logger;

    public ScanProgressReporter(ScannerWorkerMetrics metrics, ILogger<ScanProgressReporter> logger)
    {
        _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public async ValueTask ExecuteStageAsync(
        ScanJobContext context,
        string stageName,
        Func<ScanJobContext, CancellationToken, ValueTask> stageWork,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(context);
        ArgumentException.ThrowIfNullOrWhiteSpace(stageName);
        ArgumentNullException.ThrowIfNull(stageWork);

        StageStarting(_logger, context.JobId, context.ScanId, stageName, context.Lease.Attempt);

        var start = context.TimeProvider.GetUtcNow();
        using var activity = ScannerWorkerInstrumentation.ActivitySource.StartActivity(
            $"scanner.worker.{stageName}",
            ActivityKind.Internal);

        activity?.SetTag("scanner.worker.job_id", context.JobId);
        activity?.SetTag("scanner.worker.scan_id", context.ScanId);
        activity?.SetTag("scanner.worker.stage", stageName);

        try
        {
            await stageWork(context, cancellationToken).ConfigureAwait(false);
            var duration = context.TimeProvider.GetUtcNow() - start;
            _metrics.RecordStageDuration(context, stageName, duration);
            StageCompleted(_logger, context.JobId, context.ScanId, stageName, duration.TotalMilliseconds);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            StageCancelled(_logger, context.JobId, context.ScanId, stageName);
            throw;
        }
        catch (Exception ex)
        {
            var duration = context.TimeProvider.GetUtcNow() - start;
            _metrics.RecordStageDuration(context, stageName, duration);
            StageFailed(_logger, context.JobId, context.ScanId, stageName, ex);
            throw;
        }
    }

    [LoggerMessage(
        EventId = 1000,
        Level = LogLevel.Information,
        Message = "Job {JobId} (scan {ScanId}) entering stage {Stage} (attempt {Attempt}).")]
    private static partial void StageStarting(ILogger logger, string jobId, string scanId, string stage, int attempt);

    [LoggerMessage(
        EventId = 1001,
        Level = LogLevel.Information,
        Message = "Job {JobId} (scan {ScanId}) finished stage {Stage} in {ElapsedMs:F0} ms.")]
    private static partial void StageCompleted(ILogger logger, string jobId, string scanId, string stage, double elapsedMs);

    [LoggerMessage(
        EventId = 1002,
        Level = LogLevel.Warning,
        Message = "Job {JobId} (scan {ScanId}) stage {Stage} cancelled by request.")]
    private static partial void StageCancelled(ILogger logger, string jobId, string scanId, string stage);

    [LoggerMessage(
        EventId = 1003,
        Level = LogLevel.Error,
        Message = "Job {JobId} (scan {ScanId}) stage {Stage} failed.")]
    private static partial void StageFailed(ILogger logger, string jobId, string scanId, string stage, Exception exception);
}
