using System.Buffers.Binary;
using System.Diagnostics;
using System.Net.Sockets;
using System.Text;
using Microsoft.Extensions.Options;
using Tiku.Application.Assets;
using Tiku.Infrastructure.Observability;

namespace Tiku.Infrastructure.Assets;

public sealed class ClamAvOptions
{
    public const string SectionName = "Security:ClamAV";

    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 3310;
    public int TimeoutSeconds { get; set; } = 30;
    public int ChunkBytes { get; set; } = 64 * 1024;
    public long StreamMaxLength { get; set; } = 500L * 1024 * 1024;

    public static bool BeValid(ClamAvOptions options)
    {
        return !string.IsNullOrWhiteSpace(options.Host) &&
               options.Port is > 0 and <= 65535 &&
               options.TimeoutSeconds is >= 1 and <= 600 &&
               options.ChunkBytes is >= 1024 and <= 1024 * 1024 &&
               options.StreamMaxLength > 0;
    }
}

public sealed class ClamAvAssetSecurityScanner(IOptions<ClamAvOptions> options) : IAssetSecurityScanner
{
    private readonly ClamAvOptions settings = options.Value;

    public async Task<AssetSecurityScanResult> ScanAsync(
        Stream content,
        long? declaredLength,
        CancellationToken cancellationToken = default)
    {
        var startedTimestamp = Stopwatch.GetTimestamp();
        if (declaredLength > settings.StreamMaxLength)
            throw new AssetSecurityScannerException(
                "clamav_stream_too_large",
                "Asset exceeds the configured ClamAV StreamMaxLength.");

        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(settings.TimeoutSeconds));
        try
        {
            using var client = new TcpClient();
            await client.ConnectAsync(settings.Host, settings.Port, timeout.Token);
            await using var network = client.GetStream();
            await network.WriteAsync("zINSTREAM\0"u8.ToArray(), timeout.Token);
            var buffer = new byte[settings.ChunkBytes];
            var lengthBuffer = new byte[4];
            long total = 0;
            while (true)
            {
                var read = await content.ReadAsync(buffer, timeout.Token);
                if (read == 0) break;
                total += read;
                if (total > settings.StreamMaxLength)
                    throw new AssetSecurityScannerException(
                        "clamav_stream_too_large",
                        "Asset exceeds the configured ClamAV StreamMaxLength.");
                BinaryPrimitives.WriteUInt32BigEndian(lengthBuffer, (uint)read);
                await network.WriteAsync(lengthBuffer, timeout.Token);
                await network.WriteAsync(buffer.AsMemory(0, read), timeout.Token);
            }

            Array.Clear(lengthBuffer);
            await network.WriteAsync(lengthBuffer, timeout.Token);
            await network.FlushAsync(timeout.Token);
            var response = await ReadResponseAsync(network, timeout.Token);
            if (response.EndsWith(": OK", StringComparison.Ordinal))
            {
                WorkerTelemetry.RecordScan("clean", Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds);
                return new AssetSecurityScanResult(AssetSecurityScanVerdict.Clean, "clamav", null, total, response);
            }

            if (response.EndsWith(" FOUND", StringComparison.Ordinal))
            {
                var separator = response.IndexOf(": ", StringComparison.Ordinal);
                var signature = separator >= 0
                    ? response[(separator + 2)..^" FOUND".Length]
                    : "unknown";
                WorkerTelemetry.RecordScan("infected", Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds);
                return new AssetSecurityScanResult(AssetSecurityScanVerdict.Infected, "clamav", signature, total,
                    response);
            }

            throw new AssetSecurityScannerException("clamav_scan_error",
                $"ClamAV returned an error response: {response}");
        }
        catch (AssetSecurityScannerException)
        {
            WorkerTelemetry.RecordScan("error", Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds);
            throw;
        }
        catch (OperationCanceledException exception) when (!cancellationToken.IsCancellationRequested)
        {
            WorkerTelemetry.RecordScan("timeout", Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds);
            throw new AssetSecurityScannerException("clamav_timeout", "ClamAV scan timed out.", exception);
        }
        catch (Exception exception) when (exception is SocketException or IOException)
        {
            WorkerTelemetry.RecordScan("unavailable", Stopwatch.GetElapsedTime(startedTimestamp).TotalMilliseconds);
            throw new AssetSecurityScannerException("clamav_unavailable", "ClamAV is unavailable.", exception);
        }
    }

    public async Task<bool> CheckHealthAsync(CancellationToken cancellationToken = default)
    {
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
        timeout.CancelAfter(TimeSpan.FromSeconds(settings.TimeoutSeconds));
        try
        {
            using var client = new TcpClient();
            await client.ConnectAsync(settings.Host, settings.Port, timeout.Token);
            await using var network = client.GetStream();
            await network.WriteAsync("zPING\0"u8.ToArray(), timeout.Token);
            await network.FlushAsync(timeout.Token);
            return string.Equals(await ReadResponseAsync(network, timeout.Token), "PONG", StringComparison.Ordinal);
        }
        catch when (!cancellationToken.IsCancellationRequested)
        {
            return false;
        }
    }

    private static async Task<string> ReadResponseAsync(Stream stream, CancellationToken cancellationToken)
    {
        using var buffer = new MemoryStream();
        var single = new byte[1];
        while (await stream.ReadAsync(single, cancellationToken) == 1 && single[0] != 0)
        {
            buffer.WriteByte(single[0]);
            if (buffer.Length > 4096)
                throw new AssetSecurityScannerException("clamav_response_too_large",
                    "ClamAV response exceeded the safety limit.");
        }

        return Encoding.UTF8.GetString(buffer.ToArray()).Trim();
    }
}