/*
 * Happy Gopher Service
 * Copyright (c) 2026 Kyle Givler
 * Licensed under the MIT License.
 */

using HappyGopher.Extensibility;
using HappyGopher.Pages;
using HappyGopher.Telemetry;
using JoyfulReaperLib.TcpServer;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;

namespace HappyGopher.Gopher;

public sealed class GopherConnectionHandler(
    ILogger<GopherConnectionHandler> logger,
    IOptions<HappyGopherOptions> options,
    GopherPageResolver gopherPageResolver,
    GopherContentStore gopherContentStore,
    TelemetryService telemetryService) : ITcpConnectionHandler
{
    public async ValueTask HandleAsync(
        TcpConnectionContext context,
        CancellationToken cancellationToken)
    {
        GopherSessionResult? result = await ProcessAsync(
            context.ConnectionId,
            context.Stream,
            context.RemoteEndPoint,
            context.LocalEndPoint,
            cancellationToken);

        if (result is null)
        {
            return;
        }

        long connectionId = context.ConnectionId;

        context.RegisterAfterClose(afterCloseToken =>
            telemetryService.PublishSelectorServedTelemetryAsync(
                connectionId,
                result,
                afterCloseToken));
    }

    private async Task<GopherSessionResult?> ProcessAsync(
        long connectionId,
        Stream stream,
        EndPoint? remote,
        EndPoint? local,
        CancellationToken cancellationToken)
    {
        DateTimeOffset occurredAt = DateTimeOffset.UtcNow;
        var stopwatch = Stopwatch.StartNew();
        string correlationId = Guid.NewGuid().ToString("N");

        string? selector = null;
        GopherResponseKind? responseKind = null;
        bool responseCompleted = false;
        TelemetrySuppressionDecision suppressionDecision =
            TelemetrySuppressionDecision.NotSuppressed;
        CancellationTokenSource? responseTimeout = null;

        try
        {
            GopherRequest? request = await GopherSelectorReader.ReadAsync(
                stream,
                options.Value.MaxSelectorBytes,
                options.Value.MaxInputBytes,
                options.Value.RequestTimeoutSeconds,
                cancellationToken);

            if (request is null)
            {
                return null;
            }

            responseTimeout =
                CancellationTokenSource.CreateLinkedTokenSource(
                    cancellationToken);
            responseTimeout.CancelAfter(
                TimeSpan.FromSeconds(
                    options.Value.ResponseTimeoutSeconds));
            CancellationToken responseToken = responseTimeout.Token;

            request = request with
            {
                RemoteEndPoint = remote as IPEndPoint,
                LocalEndPoint = local as IPEndPoint
            };

            selector = request.Selector;

            suppressionDecision = TelemetrySuppression.Evaluate(
                remote,
                selector,
                options.Value.TelemetryIgnoredRemoteAddress,
                options.Value.TelemetryIgnoredSelectors);

            logger.LogDebug(
                "Connection {ConnectionId} from {Remote} requested selector {Selector}",
                connectionId,
                remote,
                selector);

            IGopherPage? page = gopherPageResolver.Resolve(selector);
            responseKind = page is null
                ? await gopherContentStore.WriteResponseAsync(
                    selector,
                    stream,
                    responseToken)
                : await page.WriteAsync(
                    request,
                    stream,
                    responseToken);
            responseToken.ThrowIfCancellationRequested();
            responseCompleted = true;
        }
        catch (OperationCanceledException)
            when (cancellationToken.IsCancellationRequested)
        {
            logger.LogDebug(
                "Connection {ConnectionId} from {Remote} was canceled during shutdown.",
                connectionId,
                remote);
        }
        catch (OperationCanceledException)
            when (responseTimeout?.IsCancellationRequested == true)
        {
            logger.LogWarning(
                "Response processing on connection {ConnectionId} from {Remote} timed out.",
                connectionId,
                remote);
        }
        catch (OperationCanceledException)
        {
            logger.LogWarning(
                "Connection {ConnectionId} from {Remote} timed out.",
                connectionId,
                remote);
        }
        catch (InvalidDataException exception)
        {
            logger.LogWarning(
                exception,
                "Rejected malformed request on connection {ConnectionId} from {Remote}.",
                connectionId,
                remote);
        }
        catch (IOException exception)
        {
            logger.LogDebug(
                exception,
                "Connection {ConnectionId} from {Remote} ended early.",
                connectionId,
                remote);
        }
        catch (SocketException exception)
        {
            logger.LogDebug(
                exception,
                "Socket error on connection {ConnectionId} from {Remote}.",
                connectionId,
                remote);
        }
        catch (Exception exception)
        {
            logger.LogError(
                exception,
                "Unhandled error on connection {ConnectionId} from {Remote}.",
                connectionId,
                remote);
        }
        finally
        {
            responseTimeout?.Dispose();
            stopwatch.Stop();
        }

        if (selector is null || responseKind is null)
        {
            return null;
        }

        if (suppressionDecision.IsSuppressed)
        {
            logger.LogDebug(
                "Skipping telemetry for remote endpoint {Remote}, selector {Selector}; suppression reason: {SuppressionReason}.",
                TelemetryService.FormatRemoteEndPoint(remote),
                selector,
                suppressionDecision.LogReason);

            return null;
        }

        bool succeeded =
            responseCompleted &&
            responseKind is not GopherResponseKind.InvalidSelector &&
            responseKind is not GopherResponseKind.NotFound;

        return new GopherSessionResult(
            Selector: selector,
            ResponseKind: responseKind.Value,
            Remote: TelemetryService.FormatRemoteEndPoint(remote),
            DurationMilliseconds: stopwatch.ElapsedMilliseconds,
            Succeeded: succeeded,
            OccurredAt: occurredAt,
            CorrelationId: correlationId);
    }

}
