using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;

namespace EonaCat.LogStack.Flows;

public sealed class TcpFlow : FlowBase
{
    private readonly int _batchSize;
    private const int ChannelCapacity = 4096;

    private readonly Channel<LogEvent> _channel;
    private readonly Task _senderTask;
    private readonly CancellationTokenSource _cts;

    private readonly string _host;
    private readonly int _port;
    private TcpClient? _tcpClient;
    private Stream _stream;
    private readonly bool _useTls;
    private readonly RemoteCertificateValidationCallback _certValidationCallback;
    private readonly X509CertificateCollection _clientCertificates;
    private readonly BackpressureStrategy _backpressureStrategy;

    public TcpFlow(
        string host,
        int port,
        int batchSize = 1,
        LogLevel minimumLevel = LogLevel.Trace,
        BackpressureStrategy backpressureStrategy = BackpressureStrategy.DropOldest,
        bool useTls = false,
        RemoteCertificateValidationCallback certValidationCallback = null,
        X509CertificateCollection clientCertificates = null)
        : base($"TCP:{host}:{port}", minimumLevel)
    {
        _host = host ?? throw new ArgumentNullException(nameof(host));
        _port = port;
        _backpressureStrategy = backpressureStrategy;

        _batchSize = batchSize <= 0 ? 1 : batchSize;

        _useTls = useTls;
        _certValidationCallback = certValidationCallback ?? DefaultCertificateValidation;
        _clientCertificates = clientCertificates;

        var channelOptions = new BoundedChannelOptions(ChannelCapacity)
        {
            FullMode = backpressureStrategy == BackpressureStrategy.Wait
                ? BoundedChannelFullMode.Wait
                : backpressureStrategy == BackpressureStrategy.DropNewest
                    ? BoundedChannelFullMode.DropWrite
                    : BoundedChannelFullMode.DropOldest,
            SingleReader = true,
            SingleWriter = false
        };

        _channel = Channel.CreateBounded<LogEvent>(channelOptions);
        _cts = new CancellationTokenSource();
        _senderTask = Task.Run(() => ProcessLogEventsAsync(_cts.Token));
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
    {
        if (!IsEnabled || !IsLogLevelEnabled(logEvent))
        {
            return Task.FromResult(WriteResult.LevelFiltered);
        }

        if (_channel.Writer.TryWrite(logEvent))
        {
            Interlocked.Increment(ref BlastedCount);
            return Task.FromResult(WriteResult.Success);
        }

        Interlocked.Increment(ref DroppedCount);
        return Task.FromResult(WriteResult.Dropped);
    }

    public async Task<WriteResult> SendFileAsync(string filePath, CancellationToken cancellationToken = default)
    {
        if (!IsEnabled)
        {
            return WriteResult.FlowDisabled;
        }

        if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
        {
            return WriteResult.Failed;
        }

        try
        {
            // Ensure TCP connection
            await EnsureConnectedAsync(cancellationToken);

            // Send file in chunks
            using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
            {
                byte[] buffer = new byte[4096];
                int bytesRead;
                while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
                {
                    await _stream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
                    await _stream.FlushAsync(cancellationToken);
                }
            }

            return WriteResult.Success;
        }
        catch (Exception exception)
        {
            Console.Error.WriteLine($"TcpFlow error: Error while sending file: {exception.Message}");
            return WriteResult.Failed;
        }
    }

    private static bool DefaultCertificateValidation(
        object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
    {
        return sslPolicyErrors == SslPolicyErrors.None;
    }

    public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
    {
        if (!IsEnabled)
        {
            return WriteResult.FlowDisabled;
        }

        var result = WriteResult.Success;
        foreach (var logEvent in logEvents.Span)
        {
            if (!IsLogLevelEnabled(logEvent))
            {
                continue;
            }

            if (_channel.Writer.TryWrite(logEvent))
            {
                Interlocked.Increment(ref BlastedCount);
            }
            else
            {
                Interlocked.Increment(ref DroppedCount);
                result = WriteResult.Dropped;
            }
        }

        return result;
    }

    private async Task ProcessLogEventsAsync(CancellationToken cancellationToken)
    {
        var batch = new List<LogEvent>(_batchSize);
        var sb = new StringBuilder(8192);

        while (!cancellationToken.IsCancellationRequested)
        {
            try
            {
                await EnsureConnectedAsync(cancellationToken);

                await foreach (var logEvent in _channel.Reader.ReadAllAsync(cancellationToken))
                {
                    batch.Add(logEvent);

                    if (batch.Count >= _batchSize || _channel.Reader.Count == 0)
                    {
                        await SendBatchAsync(batch, sb, cancellationToken);
                        batch.Clear();
                        sb.Clear();
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"TcpFlow error: {ex.Message}");
                await Task.Delay(1000, cancellationToken); // Retry after delay
                _tcpClient?.Dispose();
                _tcpClient = null;
            }
        }
    }

    private async Task EnsureConnectedAsync(CancellationToken cancellationToken)
    {
        if (_tcpClient != null && _tcpClient.Connected)
        {
            return;
        }

        _stream?.Dispose();
        _tcpClient?.Dispose();
        _tcpClient = null;

        _tcpClient = new TcpClient { NoDelay = true }; // lower latency
        await _tcpClient.ConnectAsync(_host, _port).ConfigureAwait(false);

        var networkStream = _tcpClient.GetStream();

        if (_useTls)
        {
            var sslStream = new SslStream(
                networkStream,
                false,
                _certValidationCallback);

            await sslStream.AuthenticateAsClientAsync(
                _host,
                _clientCertificates,
                SslProtocols.Tls12,
                checkCertificateRevocation: true).ConfigureAwait(false);

            _stream = sslStream;
        }
        else
        {
            _stream = networkStream;
        }
    }

    private async Task SendBatchAsync(List<LogEvent> batch, StringBuilder sb, CancellationToken cancellationToken)
    {
        foreach (var logEvent in batch)
        {
            FormatLogEvent(logEvent, sb);
            sb.AppendLine();
        }

        if (_stream != null)
        {
            var data = Encoding.UTF8.GetBytes(sb.ToString());
            await _stream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void FormatLogEvent(LogEvent logEvent, StringBuilder sb)
    {
        var dt = LogEvent.GetDateTime(logEvent.Timestamp);
        sb.Append(dt.ToString("yyyy-MM-dd HH:mm:ss.fff"));
        sb.Append(" [");
        sb.Append(logEvent.Level.ToString().ToUpperInvariant());
        sb.Append("] ");
        if (!string.IsNullOrEmpty(logEvent.Category))
        {
            sb.Append(logEvent.Category);
            sb.Append(": ");
        }
        sb.Append(logEvent.Message);
    }

    public override async Task FlushAsync(CancellationToken cancellationToken = default)
    {
        _channel.Writer.Complete();
        try { await _senderTask.ConfigureAwait(false); } catch { }
    }

    public override async ValueTask DisposeAsync()
    {
        IsEnabled = false;

        _channel.Writer.Complete();
        _cts.Cancel();

        try { await _senderTask.ConfigureAwait(false); } catch { }

        _stream?.Dispose();
        _tcpClient?.Dispose();
        _cts.Dispose();

        await base.DisposeAsync();
    }
}