using System.Diagnostics;
using System.Security.Claims;
using Grpc.Core;
using Satellite.V1;
using SatelliteProvider.TestSupport;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.PixelFormats;

namespace SatelliteProvider.IntegrationTests;

// AZ-492: bootstrap helpers invoked by scripts/run-performance-tests.sh.
// Each helper is a short-circuit subcommand that prints/writes its output
// and exits before the integration-test runner does any HTTP or DB work.
// All token-minting goes through the canonical JwtTokenFactory (AZ-491)
// so the shell script does NOT inline a third copy of the JWT logic.
internal static class PerfBootstrap
{
    public const string PerfSubject = "perf-tests";
    public const string GpsPermission = "GPS";
    public const string PermissionsClaimType = "permissions";
    public static readonly TimeSpan PerfTokenLifetime = TimeSpan.FromHours(4);

    public static int MintToken()
    {
        string secret;
        string issuer;
        string audience;
        try
        {
            secret = JwtTestHelpers.ResolveSecretOrThrow();
            issuer = JwtTestHelpers.ResolveIssuerOrThrow();
            audience = JwtTestHelpers.ResolveAudienceOrThrow();
        }
        catch (InvalidOperationException ex)
        {
            Console.Error.WriteLine($"--mint-only: {ex.Message}");
            return 1;
        }

        var token = JwtTokenFactory.Create(
            secret,
            PerfSubject,
            PerfTokenLifetime,
            new[] { new Claim(PermissionsClaimType, GpsPermission) },
            issuer: issuer,
            audience: audience);

        Console.Out.Write(token);
        return 0;
    }

    public static int GenerateUavFixture(string[] args)
    {
        if (args.Length < 2 || string.IsNullOrWhiteSpace(args[1]))
        {
            Console.Error.WriteLine("--gen-uav-fixture: missing output path. Usage: --gen-uav-fixture <path>");
            return 2;
        }

        var path = args[1];
        var directory = Path.GetDirectoryName(Path.GetFullPath(path));
        if (!string.IsNullOrEmpty(directory))
        {
            Directory.CreateDirectory(directory);
        }

        var bytes = CreateValidJpeg();
        File.WriteAllBytes(path, bytes);
        Console.Out.WriteLine(path);
        return 0;
    }

    // Mirrors the random-noise JPEG produced by UavUploadTests.CreateValidJpeg so
    // that the perf harness exercises the same quality-gate path as the integration
    // tests. Pixel pattern is high-variance enough to pass the UAV quality gate.
    private static byte[] CreateValidJpeg(int width = 256, int height = 256, int seed = 42)
    {
        using var image = new Image<Rgba32>(width, height);
        var random = new Random(seed);
        image.ProcessPixelRows(accessor =>
        {
            for (var y = 0; y < accessor.Height; y++)
            {
                var row = accessor.GetRowSpan(y);
                for (var x = 0; x < row.Length; x++)
                {
                    row[x] = new Rgba32(
                        (byte)random.Next(256),
                        (byte)random.Next(256),
                        (byte)random.Next(256));
                }
            }
        });

        using var stream = new MemoryStream();
        image.Save(stream, new JpegEncoder { Quality = 95 });
        return stream.ToArray();
    }

    private static bool TryResolvePerfJwt(out string token)
    {
        var preMinted = Environment.GetEnvironmentVariable("PERF_JWT_TOKEN");
        if (!string.IsNullOrWhiteSpace(preMinted))
        {
            token = preMinted;
            return true;
        }

        try
        {
            var secret = JwtTestHelpers.ResolveSecretOrThrow();
            var issuer = JwtTestHelpers.ResolveIssuerOrThrow();
            var audience = JwtTestHelpers.ResolveAudienceOrThrow();
            token = JwtTokenFactory.Create(
                secret,
                PerfSubject,
                PerfTokenLifetime,
                new[] { new Claim(PermissionsClaimType, GpsPermission) },
                issuer: issuer,
                audience: audience);
            return true;
        }
        catch (InvalidOperationException ex)
        {
            Console.Error.WriteLine($"--run-pt10: {ex.Message}");
            token = string.Empty;
            return false;
        }
    }

    public static async Task<int> RunPt10Async()
    {
        var apiUrl = Environment.GetEnvironmentVariable("API_URL") ?? "https://localhost:18980";
        var repeatCount = int.TryParse(Environment.GetEnvironmentVariable("PERF_REPEAT_COUNT"), out var n) ? n : 20;
        var slowMs = int.TryParse(Environment.GetEnvironmentVariable("PERF_PT10_SLOW_MS"), out var slow) ? slow : 50;

        if (repeatCount < 1)
        {
            Console.Error.WriteLine("--run-pt10: PERF_REPEAT_COUNT must be >= 1");
            return 1;
        }

        string token;
        if (!TryResolvePerfJwt(out token))
        {
            return 1;
        }

        RouteTileDelivery.RouteTileDeliveryClient client;
        try
        {
            client = GrpcTestHelpers.CreateClient(apiUrl, token);
        }
        catch (Exception ex)
        {
            Console.Error.WriteLine($"--run-pt10: failed to open gRPC channel to {apiUrl}: {ex.Message}");
            return 1;
        }

        var headers = GrpcTestHelpers.AuthMetadata(token);
        var firstBatchTimes = new List<long>();
        var totalStreamTimes = new List<long>();
        var failed = 0;

        for (var i = 0; i < repeatCount; i++)
        {
            var result = await ProbeStreamAsync(client, headers, Guid.NewGuid(), delayMsBetweenEvents: null);
            if (result.Success)
            {
                firstBatchTimes.Add(result.FirstBatchMs);
                totalStreamTimes.Add(result.TotalStreamMs);
                Console.Error.WriteLine($"  iteration {i + 1}/{repeatCount}: first_batch_ms={result.FirstBatchMs} total_stream_ms={result.TotalStreamMs}");
            }
            else
            {
                failed++;
                Console.Error.WriteLine($"  iteration {i + 1}/{repeatCount}: FAILED — {result.FailureReason}");
            }
        }

        if (firstBatchTimes.Count == 0)
        {
            Console.Error.WriteLine("--run-pt10: zero successful iterations");
            Console.Out.WriteLine("PT10_ITERATIONS_OK=0");
            Console.Out.WriteLine($"PT10_ITERATIONS_FAILED={failed}");
            Console.Out.WriteLine("PT10_SLOW_CONSUMER=FAIL");
            return 1;
        }

        var slowResult = await ProbeStreamAsync(client, headers, Guid.NewGuid(), delayMsBetweenEvents: slowMs);
        var slowPass = slowResult.Success && slowResult.TileCount >= 1;

        Console.Out.WriteLine($"PT10_FIRST_BATCH_P50={Percentile(firstBatchTimes, 50)}");
        Console.Out.WriteLine($"PT10_FIRST_BATCH_P95={Percentile(firstBatchTimes, 95)}");
        Console.Out.WriteLine($"PT10_TOTAL_STREAM_P50={Percentile(totalStreamTimes, 50)}");
        Console.Out.WriteLine($"PT10_TOTAL_STREAM_P95={Percentile(totalStreamTimes, 95)}");
        Console.Out.WriteLine($"PT10_ITERATIONS_OK={firstBatchTimes.Count}");
        Console.Out.WriteLine($"PT10_ITERATIONS_FAILED={failed}");
        Console.Out.WriteLine($"PT10_SLOW_CONSUMER={(slowPass ? "PASS" : "FAIL")}");
        if (!slowPass)
        {
            Console.Error.WriteLine($"--run-pt10: slow-consumer check failed — {slowResult.FailureReason ?? "no tiles received"}");
            return 1;
        }

        return failed > 0 ? 1 : 0;
    }

    internal static long Percentile(IReadOnlyList<long> values, int pct)
    {
        if (values.Count == 0)
        {
            return 0;
        }

        var sorted = values.OrderBy(v => v).ToList();
        var idx = (int)Math.Ceiling(sorted.Count * pct / 100.0) - 1;
        idx = Math.Clamp(idx, 0, sorted.Count - 1);
        return sorted[idx];
    }

    private static async Task<StreamProbeResult> ProbeStreamAsync(
        RouteTileDelivery.RouteTileDeliveryClient client,
        Metadata headers,
        Guid routeId,
        int? delayMsBetweenEvents)
    {
        var request = GrpcTestHelpers.BuildValidRequest(routeId: routeId);
        var sw = Stopwatch.StartNew();
        long firstBatchMs = -1;
        var tileCount = 0;
        DeliveryComplete? complete = null;
        DeliveryError? error = null;

        try
        {
            using var call = client.DeliverRouteTiles(request, headers);
            await foreach (var evt in call.ResponseStream.ReadAllAsync())
            {
                switch (evt.PayloadCase)
                {
                    case RouteTileEvent.PayloadOneofCase.Batch:
                        if (firstBatchMs < 0)
                        {
                            firstBatchMs = sw.ElapsedMilliseconds;
                        }

                        tileCount += evt.Batch.Tiles.Count;
                        break;
                    case RouteTileEvent.PayloadOneofCase.Complete:
                        complete = evt.Complete;
                        break;
                    case RouteTileEvent.PayloadOneofCase.Error:
                        error = evt.Error;
                        break;
                }

                if (delayMsBetweenEvents.HasValue)
                {
                    await Task.Delay(delayMsBetweenEvents.Value);
                }
            }
        }
        catch (RpcException ex)
        {
            return StreamProbeResult.Fail($"RpcException {ex.StatusCode}: {ex.Status.Detail}");
        }

        var totalStreamMs = sw.ElapsedMilliseconds;

        if (error is not null)
        {
            return StreamProbeResult.Fail($"DeliveryError {error.Code}: {error.Message}");
        }

        if (complete is null)
        {
            return StreamProbeResult.Fail("stream ended without DeliveryComplete");
        }

        if (firstBatchMs < 0)
        {
            return StreamProbeResult.Fail("no Batch event before DeliveryComplete");
        }

        return StreamProbeResult.Ok(firstBatchMs, totalStreamMs, tileCount);
    }

    private readonly record struct StreamProbeResult(
        bool Success,
        long FirstBatchMs,
        long TotalStreamMs,
        int TileCount,
        string? FailureReason)
    {
        public static StreamProbeResult Ok(long firstBatchMs, long totalStreamMs, int tileCount) =>
            new(true, firstBatchMs, totalStreamMs, tileCount, null);

        public static StreamProbeResult Fail(string reason) =>
            new(false, 0, 0, 0, reason);
    }
}
