# frozen_string_literal: true

# Released under the MIT License.
# Copyright, 2018-2026, by Samuel Williams.

require_relative "../response"
require_relative "stream"

module Async
	module HTTP
		module Protocol
			module HTTP2
				# Typically used on the client side for writing a request and reading the incoming response.
				class Response < Protocol::Response
					# Represents the HTTP/2 stream associated with an outgoing client-side response.
					class Stream < HTTP2::Stream
						# Initialize the response stream.
						def initialize(*)
							super
							
							@response = Response.new(self)
							
							@notification = Async::Notification.new
							@exception = nil
						end
						
						attr :response
						
						# Wait for the response headers and return the response body.
						# @returns [Protocol::HTTP::Body::Readable | Nil] The response body.
						def wait_for_input
							# The input isn't ready until the response headers have been received:
							@response.wait
							
							# There is a possible race condition if you try to access @input - it might already be closed and nil.
							return @response.body
						end
						
						# Handle a push promise stream from the server.
						# @parameter promised_stream_id [Integer] The stream ID for the promised resource.
						# @parameter headers [Array] The promise headers.
						def accept_push_promise_stream(promised_stream_id, headers)
							raise ProtocolError, "Cannot accept push promise stream!"
						end
						
						# This should be invoked from the background reader, and notifies the task waiting for the headers that we are done.
						def receive_initial_headers(headers, end_stream)
							# While in theory, the response pseudo-headers may be extended in the future, currently they only response pseudo-header is :status, so we can assume it is always the first header.
							status_header = headers.shift
							
							if status_header.first != ":status"
								raise ProtocolError, "Invalid response headers: #{headers.inspect}"
							end
							
							status = Integer(status_header.last)
							
							if status >= 100 && status < 200
								return receive_interim_headers(status, headers)
							end
							
							@response.status = status
							@headers = ::Protocol::HTTP::Headers.new
							
							# If the protocol request was successful, ensure the response protocol matches:
							if status == 200 and protocol = @response.request.protocol
								@response.protocol = Array(protocol).first
							end
							
							headers.each do |key, value|
								# It's guaranteed that this should be the first header:
								if key == CONTENT_LENGTH
									@length = Integer(value)
								else
									add_header(key, value)
								end
							end
							
							@response.headers = @headers
							
							if @response.valid?
								if !end_stream
									# We only construct the input/body if data is coming.
									@response.body = prepare_input(@length)
								elsif @response.head?
									@response.body = ::Protocol::HTTP::Body::Head.new(@length)
								end
							else
								send_reset_stream(::Protocol::HTTP2::Error::PROTOCOL_ERROR)
							end
							
							self.notify!
							
							return headers
						end
						
						# Process interim (1xx) response headers.
						# @parameter status [Integer] The interim status code.
						# @parameter headers [Array] The interim response headers.
						def receive_interim_headers(status, headers)
							if headers.any?
								headers = ::Protocol::HTTP::Headers[headers]
							else
								headers = nil
							end
							
							@response.request.send_interim_response(status, headers)
						end
						
						# Notify anyone waiting on the response headers to be received (or failure).
						def notify!
							if notification = @notification
								@notification = nil
								notification.signal
							end
						end
						
						# Wait for the headers to be received or for stream reset.
						def wait
							# If you call wait after the headers were already received, it should return immediately:
							@notification&.wait
							
							if @exception
								raise @exception
							end
						end
						
						# Called when the stream is closed.
						# @parameter error [Exception | Nil] The error that caused the close, if any.
						def closed(error)
							super
							
							if @response
								@response = nil
							end
							
							@exception = error
							
							self.notify!
						end
					end
					
					# Initialize the response from an HTTP/2 stream.
					# @parameter stream [Stream] The HTTP/2 stream for this response.
					def initialize(stream)
						super(stream.connection.version, nil, nil)
						
						@stream = stream
						@request = nil
					end
					
					attr :stream
					attr :request
					
					# Assign the connection pool, releasing the connection when the stream is closed.
					# @parameter pool [Async::Pool::Controller] The connection pool.
					def pool=(pool)
						# If we are already closed, the stream can be released now:
						if @stream.closed?
							pool.release(@stream.connection)
						else
							# Otherwise, we will release the stream when it is closed:
							@stream.pool = pool
						end
					end
					
					# @returns [Connection] The underlying HTTP/2 connection.
					def connection
						@stream.connection
					end
					
					# Wait for the response headers to be received.
					def wait
						@stream.wait
					end
					
					# @returns [Boolean] Whether the original request was a HEAD request.
					def head?
						@request&.head?
					end
					
					# @returns [Boolean] Whether the response has a valid status.
					def valid?
						!!@status
					end
					
					# Build a request object from push promise headers.
					# @parameter headers [Array] The push promise pseudo-headers and headers.
					# @returns [Protocol::HTTP::Request] The constructed request.
					def build_request(headers)
						request = ::Protocol::HTTP::Request.new
						request.headers = ::Protocol::HTTP::Headers.new
						
						headers.each do |key, value|
							if key == SCHEME
								raise ::Protocol::HTTP2::HeaderError, "Request scheme already specified!" if request.scheme
								
								request.scheme = value
							elsif key == AUTHORITY
								raise ::Protocol::HTTP2::HeaderError, "Request authority already specified!" if request.authority
								
								request.authority = value
							elsif key == METHOD
								raise ::Protocol::HTTP2::HeaderError, "Request method already specified!" if request.method
								
								request.method = value
							elsif key == PATH
								raise ::Protocol::HTTP2::HeaderError, "Request path is empty!" if value.empty?
								raise ::Protocol::HTTP2::HeaderError, "Request path already specified!" if request.path
								
								request.path = value
							elsif key.start_with? ":"
								raise ::Protocol::HTTP2::HeaderError, "Invalid pseudo-header #{key}!"
							else
								request.headers[key] = value
							end
						end
						
						@request = request
					end
					
					# Send a request and read it into this response.
					def send_request(request)
						@request = request
						
						# https://http2.github.io/http2-spec/#rfc.section.8.1.2.3
						# All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header fields is malformed (Section 8.1.2.6).
						pseudo_headers = [
							[SCHEME, request.scheme],
							[METHOD, request.method],
						]
						
						if path = request.path
							pseudo_headers << [PATH, path]
						end
						
						# To ensure that the HTTP/1.1 request line can be reproduced accurately, this pseudo-header field MUST be omitted when translating from an HTTP/1.1 request that has a request target in origin or asterisk form (see [RFC7230], Section 5.3). Clients that generate HTTP/2 requests directly SHOULD use the :authority pseudo-header field instead of the Host header field.
						if authority = request.authority
							pseudo_headers << [AUTHORITY, authority]
						end
						
						if protocol = request.protocol
							pseudo_headers << [PROTOCOL, protocol]
						end
						
						headers = ::Protocol::HTTP::Headers::Merged.new(
							pseudo_headers,
							request.headers.header
						)
						
						if request.body.nil?
							@stream.send_headers(headers, ::Protocol::HTTP2::END_STREAM)
						else
							if length = request.body.length
								# This puts it at the end of the pseudo-headers:
								pseudo_headers << [CONTENT_LENGTH, length]
							end
							
							# This function informs the headers object that any subsequent headers are going to be trailer. Therefore, it must be called *before* sending the headers, to avoid any race conditions.
							trailer = request.headers.trailer!
							
							begin
								@stream.send_headers(headers)
							rescue
								raise ::Protocol::HTTP::RefusedError
							end
							
							@stream.send_body(request.body, trailer)
						end
					end
				end
			end
		end
	end
end
