using System.Globalization;

namespace Sunfish.Http;

/// <summary>Base de los cuerpos de peticion: solo lectura asincrona.</summary>
internal abstract class RequestBodyStream : Stream
{
    private Func<Task>? _onFirstRead;

    protected RequestBodyStream(HttpRequestReader reader, Func<Task>? onFirstRead)
    {
        Reader = reader;
        _onFirstRead = onFirstRead;
    }

    protected HttpRequestReader Reader { get; }

    /// <summary>Se ha leido el cuerpo entero.</summary>
    public abstract bool Completed { get; }

    public override async ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken ct = default)
    {
        if (_onFirstRead is not null)
        {
            // Expect: 100-continue: el cliente espera permiso antes de enviar el cuerpo.
            var send = _onFirstRead;
            _onFirstRead = null;
            await send();
        }
        return await ReadBodyAsync(buffer, ct);
    }

    protected abstract ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct);

    public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken ct) =>
        ReadAsync(buffer.AsMemory(offset, count), ct).AsTask();

    public override int Read(byte[] buffer, int offset, int count) =>
        ReadAsync(buffer.AsMemory(offset, count)).AsTask().GetAwaiter().GetResult();

    /// <summary>Descarta lo que quede (hasta un limite) para poder leer la siguiente peticion.</summary>
    public async Task<bool> DrainAsync(long limit, CancellationToken ct)
    {
        var buffer = new byte[16384];
        long total = 0;
        while (!Completed)
        {
            int n = await ReadBodyAsync(buffer, ct);
            if (n == 0)
                return Completed;
            total += n;
            if (total > limit)
                return false;
        }
        return true;
    }

    public override bool CanRead => true;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotSupportedException();
    public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); }
    public override void Flush() { }
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();
    public override void SetLength(long value) => throw new NotSupportedException();
    public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
}

/// <summary>Cuerpo con Content-Length.</summary>
internal sealed class ContentLengthReadStream(HttpRequestReader reader, long length, Func<Task>? onFirstRead)
    : RequestBodyStream(reader, onFirstRead)
{
    private long _remaining = length;

    public override bool Completed => _remaining == 0;

    protected override async ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct)
    {
        if (_remaining == 0)
            return 0;
        if (buffer.Length > _remaining)
            buffer = buffer[..(int)_remaining];
        int n = await Reader.ReadRawAsync(buffer, ct);
        if (n == 0)
            throw new IOException("Connection closed in body");
        _remaining -= n;
        return n;
    }
}

/// <summary>Cuerpo con Transfer-Encoding: chunked.</summary>
internal sealed class ChunkedReadStream(HttpRequestReader reader, Func<Task>? onFirstRead)
    : RequestBodyStream(reader, onFirstRead)
{
    private long _chunkRemaining;
    private bool _done;

    public override bool Completed => _done;

    protected override async ValueTask<int> ReadBodyAsync(Memory<byte> buffer, CancellationToken ct)
    {
        if (_done)
            return 0;
        if (_chunkRemaining == 0)
        {
            string line = await Reader.ReadLineAsync(ct, countAsHeader: false) ?? throw new IOException("Connection closed in chunk");
            int semi = line.IndexOf(';');
            string size = (semi < 0 ? line : line[..semi]).Trim();
            if (!long.TryParse(size, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out _chunkRemaining) || _chunkRemaining < 0)
                throw new HttpParseException(400, "Bad chunk size");
            if (_chunkRemaining == 0)
            {
                // Trailers hasta la linea vacia.
                while (!string.IsNullOrEmpty(await Reader.ReadLineAsync(ct, countAsHeader: false))) { }
                _done = true;
                return 0;
            }
        }
        if (buffer.Length > _chunkRemaining)
            buffer = buffer[..(int)_chunkRemaining];
        int n = await Reader.ReadRawAsync(buffer, ct);
        if (n == 0)
            throw new IOException("Connection closed in chunk");
        _chunkRemaining -= n;
        if (_chunkRemaining == 0)
            await Reader.ReadLineAsync(ct, countAsHeader: false); // CRLF tras los datos del trozo
        return n;
    }
}
