using System.Globalization;
using System.Text;

namespace Sunfish.Http;

/// <summary>
/// Respuesta HTTP. Las cabeceras se envian al empezar el cuerpo. Con longitud conocida se usa
/// Content-Length (los clientes antiguos no conocen chunked y asi muestran el progreso); sin ella,
/// chunked para HTTP/1.1 y cierre de conexion para 1.0. HTTP/0.9 recibe solo el cuerpo.
/// </summary>
public sealed class HttpResponse
{
    private readonly Stream _stream;
    private readonly HttpRequest _request;
    private Stream? _body;

    internal HttpResponse(Stream stream, HttpRequest request)
    {
        _stream = stream;
        _request = request;
        KeepAlive = request.WantsKeepAlive;
    }

    public int StatusCode { get; set; } = 200;

    public HttpHeaders Headers { get; } = new();

    public bool HeadersSent => _body is not null;

    /// <summary>Si la conexion seguira abierta despues de esta respuesta.</summary>
    public bool KeepAlive { get; private set; }

    /// <summary>Bytes de cuerpo enviados (para el registro).</summary>
    public long BytesSent => _body is CountingStream c ? c.Count : 0;

    /// <summary>Envia la cabecera y devuelve el flujo para escribir el cuerpo.</summary>
    public async Task<Stream> StartAsync(long? contentLength, CancellationToken ct = default)
    {
        if (_body is not null)
            throw new InvalidOperationException("Response already started");

        bool bodyAllowed = StatusCode is not (204 or 304) && StatusCode >= 200;
        bool chunked = false;
        if (bodyAllowed && contentLength is long length)
        {
            Headers["Content-Length"] = length.ToString(CultureInfo.InvariantCulture);
        }
        else if (bodyAllowed)
        {
            if (_request.IsHttp11OrLater)
            {
                Headers["Transfer-Encoding"] = "chunked";
                chunked = true;
            }
            else
            {
                KeepAlive = false; // 1.0 sin longitud: el final del cuerpo es el cierre
            }
        }

        if (!_request.IsHttp09)
        {
            if (KeepAlive)
            {
                if (!_request.IsHttp11OrLater)
                    Headers["Connection"] = "keep-alive";
            }
            else
            {
                Headers["Connection"] = "close";
            }
            Headers["Date"] ??= FormatDate(DateTime.UtcNow);
            Headers["Server"] ??= "Sunfish/3.0";
            await _stream.WriteAsync(BuildHead(), ct);
        }

        Stream body = _stream;
        if (_request.IsHead || !bodyAllowed)
            body = Stream.Null;
        else if (chunked)
            body = new ChunkedWriteStream(_stream);
        _body = new CountingStream(body);
        return _body;
    }

    /// <summary>Respuesta completa con el cuerpo en memoria.</summary>
    public async Task SendAsync(byte[] content, string? contentType, CancellationToken ct = default)
    {
        if (contentType is not null)
            Headers["Content-Type"] = contentType;
        var body = await StartAsync(content.Length, ct);
        await body.WriteAsync(content, ct);
    }

    /// <summary>Termina la respuesta: si no se envio nada, una vacia; si era chunked, el trozo final.</summary>
    internal async Task CompleteAsync(CancellationToken ct)
    {
        if (_body is null)
            await StartAsync(0, ct);
        if (_body is CountingStream { Inner: ChunkedWriteStream chunked })
            await chunked.FinishAsync(ct);
        await _stream.FlushAsync(ct);
    }

    private byte[] BuildHead()
    {
        var sb = new StringBuilder();
        string version = _request.IsHttp11OrLater ? "HTTP/1.1" : "HTTP/1.0";
        sb.Append(version).Append(' ').Append(StatusCode.ToString(CultureInfo.InvariantCulture))
          .Append(' ').Append(ReasonPhrase(StatusCode)).Append("\r\n");
        foreach (var (name, value) in Headers.All)
            sb.Append(name).Append(": ").Append(value).Append("\r\n");
        sb.Append("\r\n");
        return Encoding.Latin1.GetBytes(sb.ToString());
    }

    public static string FormatDate(DateTime utc) =>
        utc.ToString("r", CultureInfo.InvariantCulture);

    public static string ReasonPhrase(int status) => status switch
    {
        100 => "Continue",
        200 => "OK",
        201 => "Created",
        204 => "No Content",
        206 => "Partial Content",
        207 => "Multi-Status",
        301 => "Moved Permanently",
        302 => "Found",
        304 => "Not Modified",
        400 => "Bad Request",
        401 => "Unauthorized",
        403 => "Forbidden",
        404 => "Not Found",
        405 => "Method Not Allowed",
        409 => "Conflict",
        412 => "Precondition Failed",
        413 => "Payload Too Large",
        414 => "URI Too Long",
        416 => "Range Not Satisfiable",
        431 => "Request Header Fields Too Large",
        500 => "Internal Server Error",
        501 => "Not Implemented",
        503 => "Service Unavailable",
        505 => "HTTP Version Not Supported",
        _ => "Unknown",
    };
}

/// <summary>Cuenta los bytes escritos.</summary>
internal sealed class CountingStream(Stream inner) : Stream
{
    public Stream Inner { get; } = inner;
    public long Count { get; private set; }

    public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken ct = default)
    {
        await Inner.WriteAsync(buffer, ct);
        Count += buffer.Length;
    }

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

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

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

/// <summary>Escritura con Transfer-Encoding: chunked.</summary>
internal sealed class ChunkedWriteStream(Stream inner) : Stream
{
    public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken ct = default)
    {
        if (buffer.Length == 0)
            return; // un trozo vacio marcaria el final
        await inner.WriteAsync(Encoding.ASCII.GetBytes(buffer.Length.ToString("X", CultureInfo.InvariantCulture) + "\r\n"), ct);
        await inner.WriteAsync(buffer, ct);
        await inner.WriteAsync("\r\n"u8.ToArray(), ct);
    }

    public Task FinishAsync(CancellationToken ct) => inner.WriteAsync("0\r\n\r\n"u8.ToArray(), ct).AsTask();

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

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

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