using System.Collections.Generic;
using System.IO.Pipelines;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Text;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using StellaOps.Scanner.WebService.Constants;
using StellaOps.Scanner.WebService.Contracts;
using StellaOps.Scanner.WebService.Domain;
using StellaOps.Scanner.WebService.Infrastructure;
using StellaOps.Scanner.WebService.Security;
using StellaOps.Scanner.WebService.Services;

namespace StellaOps.Scanner.WebService.Endpoints;

internal static class ScanEndpoints
{
    private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
    {
        Converters = { new JsonStringEnumConverter() }
    };

    public static void MapScanEndpoints(this RouteGroupBuilder apiGroup)
    {
        ArgumentNullException.ThrowIfNull(apiGroup);

        var scans = apiGroup.MapGroup("/scans");

        scans.MapPost("/", HandleSubmitAsync)
            .WithName("scanner.scans.submit")
            .Produces<ScanSubmitResponse>(StatusCodes.Status202Accepted)
            .Produces(StatusCodes.Status400BadRequest)
            .Produces(StatusCodes.Status409Conflict)
            .RequireAuthorization(ScannerPolicies.ScansEnqueue);

        scans.MapGet("/{scanId}", HandleStatusAsync)
            .WithName("scanner.scans.status")
            .Produces<ScanStatusResponse>(StatusCodes.Status200OK)
            .Produces(StatusCodes.Status404NotFound)
            .RequireAuthorization(ScannerPolicies.ScansRead);

        scans.MapGet("/{scanId}/events", HandleProgressStreamAsync)
            .WithName("scanner.scans.events")
            .Produces(StatusCodes.Status200OK)
            .Produces(StatusCodes.Status404NotFound)
            .RequireAuthorization(ScannerPolicies.ScansRead);
    }

    private static async Task<IResult> HandleSubmitAsync(
        ScanSubmitRequest request,
        IScanCoordinator coordinator,
        LinkGenerator links,
        HttpContext context,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(request);
        ArgumentNullException.ThrowIfNull(coordinator);
        ArgumentNullException.ThrowIfNull(links);

        if (request.Image is null)
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.Validation,
                "Invalid scan submission",
                StatusCodes.Status400BadRequest,
                detail: "Request image descriptor is required.");
        }

        var reference = request.Image.Reference;
        var digest = request.Image.Digest;
        if (string.IsNullOrWhiteSpace(reference) && string.IsNullOrWhiteSpace(digest))
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.Validation,
                "Invalid scan submission",
                StatusCodes.Status400BadRequest,
                detail: "Either image.reference or image.digest must be provided.");
        }

        if (!string.IsNullOrWhiteSpace(digest) && !digest.Contains(':', StringComparison.Ordinal))
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.Validation,
                "Invalid scan submission",
                StatusCodes.Status400BadRequest,
                detail: "Image digest must include algorithm prefix (e.g. sha256:...).");
        }

        var target = new ScanTarget(reference, digest).Normalize();
        var metadata = NormalizeMetadata(request.Metadata);
        var submission = new ScanSubmission(
            Target: target,
            Force: request.Force,
            ClientRequestId: request.ClientRequestId?.Trim(),
            Metadata: metadata);

        ScanSubmissionResult result;
        try
        {
            result = await coordinator.SubmitAsync(submission, context.RequestAborted).ConfigureAwait(false);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }

        var statusText = result.Snapshot.Status.ToString();
        var location = links.GetPathByName(
            httpContext: context,
            endpointName: "scanner.scans.status",
            values: new { scanId = result.Snapshot.ScanId.Value });

        if (!string.IsNullOrWhiteSpace(location))
        {
            context.Response.Headers.Location = location;
        }

        var response = new ScanSubmitResponse(
            ScanId: result.Snapshot.ScanId.Value,
            Status: statusText,
            Location: location,
            Created: result.Created);

        return Json(response, StatusCodes.Status202Accepted);
    }

    private static async Task<IResult> HandleStatusAsync(
        string scanId,
        IScanCoordinator coordinator,
        HttpContext context,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(coordinator);

        if (!ScanId.TryParse(scanId, out var parsed))
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.Validation,
                "Invalid scan identifier",
                StatusCodes.Status400BadRequest,
                detail: "Scan identifier is required.");
        }

        var snapshot = await coordinator.GetAsync(parsed, context.RequestAborted).ConfigureAwait(false);
        if (snapshot is null)
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.NotFound,
                "Scan not found",
                StatusCodes.Status404NotFound,
                detail: "Requested scan could not be located.");
        }

        var response = new ScanStatusResponse(
            ScanId: snapshot.ScanId.Value,
            Status: snapshot.Status.ToString(),
            Image: new ScanStatusTarget(snapshot.Target.Reference, snapshot.Target.Digest),
            CreatedAt: snapshot.CreatedAt,
            UpdatedAt: snapshot.UpdatedAt,
            FailureReason: snapshot.FailureReason);

        return Json(response, StatusCodes.Status200OK);
    }

    private static async Task<IResult> HandleProgressStreamAsync(
        string scanId,
        string? format,
        IScanProgressReader progressReader,
        HttpContext context,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(progressReader);

        if (!ScanId.TryParse(scanId, out var parsed))
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.Validation,
                "Invalid scan identifier",
                StatusCodes.Status400BadRequest,
                detail: "Scan identifier is required.");
        }

        if (!progressReader.Exists(parsed))
        {
            return ProblemResultFactory.Create(
                context,
                ProblemTypes.NotFound,
                "Scan not found",
                StatusCodes.Status404NotFound,
                detail: "Requested scan could not be located.");
        }

        var streamFormat = string.Equals(format, "jsonl", StringComparison.OrdinalIgnoreCase)
            ? "jsonl"
            : "sse";

        context.Response.StatusCode = StatusCodes.Status200OK;
        context.Response.Headers.CacheControl = "no-store";
        context.Response.Headers["X-Accel-Buffering"] = "no";
        context.Response.Headers["Connection"] = "keep-alive";

        if (streamFormat == "jsonl")
        {
            context.Response.ContentType = "application/x-ndjson";
        }
        else
        {
            context.Response.ContentType = "text/event-stream";
        }

        await foreach (var progressEvent in progressReader.SubscribeAsync(parsed, context.RequestAborted).WithCancellation(context.RequestAborted))
        {
            var payload = new
            {
                scanId = progressEvent.ScanId.Value,
                sequence = progressEvent.Sequence,
                state = progressEvent.State,
                message = progressEvent.Message,
                timestamp = progressEvent.Timestamp,
                correlationId = progressEvent.CorrelationId,
                data = progressEvent.Data
            };

            if (streamFormat == "jsonl")
            {
                await WriteJsonLineAsync(context.Response.BodyWriter, payload, cancellationToken).ConfigureAwait(false);
            }
            else
            {
                await WriteSseAsync(context.Response.BodyWriter, payload, progressEvent, cancellationToken).ConfigureAwait(false);
            }

            await context.Response.BodyWriter.FlushAsync(cancellationToken).ConfigureAwait(false);
        }

        return Results.Empty;
    }

    private static IReadOnlyDictionary<string, string> NormalizeMetadata(IDictionary<string, string> metadata)
    {
        if (metadata is null || metadata.Count == 0)
        {
            return new Dictionary<string, string>();
        }

        var normalized = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        foreach (var pair in metadata)
        {
            if (string.IsNullOrWhiteSpace(pair.Key))
            {
                continue;
            }

            var key = pair.Key.Trim();
            var value = pair.Value?.Trim() ?? string.Empty;
            normalized[key] = value;
        }

        return normalized;
    }

    private static async Task WriteJsonLineAsync(PipeWriter writer, object payload, CancellationToken cancellationToken)
    {
        var json = JsonSerializer.Serialize(payload, SerializerOptions);
        var jsonBytes = Encoding.UTF8.GetBytes(json);
        await writer.WriteAsync(jsonBytes, cancellationToken).ConfigureAwait(false);
        await writer.WriteAsync(new[] { (byte)'\n' }, cancellationToken).ConfigureAwait(false);
    }

    private static async Task WriteSseAsync(PipeWriter writer, object payload, ScanProgressEvent progressEvent, CancellationToken cancellationToken)
    {
        var json = JsonSerializer.Serialize(payload, SerializerOptions);
        var eventName = progressEvent.State.ToLowerInvariant();
        var builder = new StringBuilder();
        builder.Append("id: ").Append(progressEvent.Sequence).Append('\n');
        builder.Append("event: ").Append(eventName).Append('\n');
        builder.Append("data: ").Append(json).Append('\n');
        builder.Append('\n');

        var bytes = Encoding.UTF8.GetBytes(builder.ToString());
        await writer.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
    }

    private static IResult Json<T>(T value, int statusCode)
    {
        var payload = JsonSerializer.Serialize(value, SerializerOptions);
        return Results.Content(payload, "application/json", System.Text.Encoding.UTF8, statusCode);
    }
}
