namespace ErsatzTV.Mcp;

/// <summary>
/// Reads newline-delimited lines from a <see cref="TextReader"/> with a hard character cap, so a
/// hostile client cannot exhaust memory by sending an enormous line with no newline. A line longer
/// than the cap is drained (not buffered) and reported as overflowed rather than returned.
/// </summary>
public static class BoundedLineReader
{
    public readonly record struct Line(bool EndOfStream, bool Overflowed, string Text);

    public const int DefaultMaxChars = 1024 * 1024;

    public static async Task<Line> ReadLineAsync(TextReader reader, int maxChars = DefaultMaxChars)
    {
        int cap = maxChars > 0 ? maxChars : DefaultMaxChars;
        var builder = new System.Text.StringBuilder();
        var buffer = new char[1];
        bool sawAny = false;
        bool overflowed = false;

        while (await reader.ReadAsync(buffer, 0, 1) == 1)
        {
            sawAny = true;
            char c = buffer[0];
            if (c == '\n')
            {
                return new Line(false, overflowed, overflowed ? string.Empty : builder.ToString());
            }

            if (c == '\r')
            {
                continue;
            }

            if (overflowed || builder.Length >= cap)
            {
                // Past the cap: stop buffering and free what we have, but keep draining to the
                // newline so the next line stays aligned.
                overflowed = true;
                builder.Clear();
                continue;
            }

            builder.Append(c);
        }

        if (!sawAny)
        {
            return new Line(true, false, string.Empty);
        }

        // Final line with no trailing newline.
        return new Line(false, overflowed, overflowed ? string.Empty : builder.ToString());
    }
}
