# frozen_string_literal: true

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

require "console"

require_relative "../constants"
require_relative "../input"
require_relative "../response"
require_relative "../rewindable"

module Protocol
	module Rack
		module Adapter
			# The base adapter class that provides common functionality for all Rack adapters.
			# It handles the conversion between {Protocol::HTTP} and Rack environments.
			class Generic
				# Creates a new adapter instance for the given Rack application.
				# Wraps the adapter in a {Rewindable} instance to ensure request body can be read multiple times, which is required for Rack < 3.
				# 
				# @parameter app [Interface(:call)] A Rack application.
				# @returns [Rewindable] A rewindable adapter instance.
				def self.wrap(app)
					Rewindable.new(self.new(app))
				end
				
				# Parses a Rackup file and returns the application.
				# 
				# @parameter path [String] The path to the Rackup file.
				# @returns [Interface(:call)] The Rack application.
				def self.parse_file(...)
					# This is the old interface, which was changed in Rack 3:
					::Rack::Builder.parse_file(...).first
				end
				
				# Initialize the Rack adapter middleware.
				# 
				# @parameter app [Interface(:call)] The Rack middleware.
				# @raises [ArgumentError] If the app does not respond to `call`.
				def initialize(app)
					@app = app
					
					raise ArgumentError, "App must be callable!" unless @app.respond_to?(:call)
				end
				
				# The logger to use for this adapter.
				# 
				# @returns [Console] The console logger.
				def logger
					Console
				end
				
				# Unwrap HTTP headers into the CGI-style expected by Rack middleware, and add them to the Rack `env`.
				#
				# For example, `accept-encoding` becomes `HTTP_ACCEPT_ENCODING`.
				#
				# Header keys with underscores will generate the same CGI-style header key as headers with dashes.
				#
				# For example, `accept_encoding` becomes `HTTP_ACCEPT_ENCODING` too.
				#
				# You should not implicitly trust the `HTTP_` headers for security purposes, as they are generated by the client.
				#
				# Multiple headers are combined with a comma, with one exception: `HTTP_COOKIE` headers are combined with a semicolon.
				#
				# @parameter headers [Protocol::HTTP::Headers] The raw HTTP request headers.
				# @parameter env [Hash] The Rack request `env`.
				def unwrap_headers(headers, env)
					headers.each do |key, value|
						http_key = "HTTP_#{key.upcase.tr('-', '_')}"
						
						if current_value = env[http_key]
							if http_key == CGI::HTTP_COOKIE
								env[http_key] = "#{current_value};#{value}"
							else
								env[http_key] = "#{current_value},#{value}"
							end
						else
							env[http_key] = value.to_s
						end
					end
				end
				
				# Process the incoming request into a valid Rack `env`.
				#
				# - Set the `env['CONTENT_TYPE']` and `env['CONTENT_LENGTH']` based on the incoming request body. 
				# - Set the `env['HTTP_HOST']` header to the request authority.
				# - Set the `env['HTTP_X_FORWARDED_PROTO']` header to the request scheme.
				# - Set `env['REMOTE_ADDR']` to the request remote address.
				#
				# @parameter request [Protocol::HTTP::Request] The incoming request.
				# @parameter env [Hash] The rack `env`.
				def unwrap_request(request, env)
					# The request protocol, either from the upgrade header or the HTTP/2 pseudo header of the same name:
					if protocol = request.protocol
						env[RACK_PROTOCOL] = protocol
					end
					
					if content_type = request.headers.delete("content-type")
						env[CGI::CONTENT_TYPE] = content_type
					end
					
					# In some situations we don't know the content length, e.g. when using chunked encoding, or when decompressing the body:
					if body = request.body and length = body.length
						env[CGI::CONTENT_LENGTH] = length.to_s
					end
					
					# We ignore trailers for the purpose of constructing the rack environment:
					self.unwrap_headers(request.headers.header, env)
					
					# For the sake of compatibility, we set the `HTTP_UPGRADE` header to the requested protocol:
					if protocol = request.protocol and request.version.start_with?("HTTP/1")
						env[CGI::HTTP_UPGRADE] = Array(protocol).join(",")
					end
					
					if request.respond_to?(:hijack?) and request.hijack?
						env[RACK_IS_HIJACK] = true
						env[RACK_HIJACK] = proc{request.hijack!.io}
					end
					
					# HTTP/2 prefers `:authority` over `host`, so we do this for backwards compatibility:
					env[CGI::HTTP_HOST] ||= request.authority
					
					if peer = request.peer
						env[CGI::REMOTE_ADDR] = peer.ip_address
					end
				end
				
				# Create a base environment hash for the request.
				# 
				# @parameter request [Protocol::HTTP::Request] The incoming request.
				# @returns [Hash] The base environment hash.
				def make_environment(request)
					{
						request: request
					}
				end
				
				# Handle errors that occur during request processing. Logs the error, closes any response body, invokes `rack.response_finished` callbacks, and returns an appropriate failure response.
				# 
				# The `rack.response_finished` callbacks are invoked in reverse order of registration, as specified by the Rack specification. If a callback raises an exception, it is caught and logged, but does not prevent other callbacks from being invoked.
				# 
				# @parameter env [Hash] The Rack environment hash.
				# @parameter status [Integer | Nil] The HTTP status code, if available. May be `nil` if the error occurred before the application returned a response.
				# @parameter headers [Hash | Nil] The response headers, if available. May be `nil` if the error occurred before the application returned a response.
				# @parameter body [Object | Nil] The response body, if available. May be `nil` if the error occurred before the application returned a response.
				# @parameter error [Exception] The exception that occurred during request processing.
				# @returns [Protocol::HTTP::Response] A failure response representing the error.
				def handle_error(env, status, headers, body, error)
					Console.error(self, "Error occurred during request processing:", error)
					
					# Close the response body if it exists and supports closing:
					body&.close if body.respond_to?(:close)
					
					# Invoke `rack.response_finished` callbacks in reverse order of registration. This ensures that callbacks registered later are invoked first, matching the Rack specification.
					env&.[](RACK_RESPONSE_FINISHED)&.reverse_each do |callback|
						begin
							callback.call(env, status, headers, error)
						rescue => callback_error
							# If a callback raises an exception, log it but continue invoking other callbacks. The Rack specification states that callbacks should not raise exceptions, but we handle this gracefully to prevent one misbehaving callback from breaking others.
							Console.error(self, "Error occurred during response finished callback:", callback_error)
						end
					end
					
					return failure_response(error)
				end
				
				# Build a Rack `env` from the incoming request and apply it to the Rack middleware.
				#
				# @parameter request [Protocol::HTTP::Request] The incoming request.
				# @returns [Protocol::HTTP::Response] The HTTP response.
				# @raises [ArgumentError] If the status is not an integer or headers are nil.
				def call(request)
					env = self.make_environment(request)
					
					status, headers, body = @app.call(env)
					
					# The status must always be an integer:
					unless status.is_a?(Integer)
						raise ArgumentError, "Status must be an integer!"
					end
					
					# Headers must always be a hash or equivalent:
					unless headers
						raise ArgumentError, "Headers must not be nil!"
					end
					
					headers, meta = self.wrap_headers(headers)
					
					return Response.wrap(env, status, headers, meta, body, request)
				rescue => error
					return self.handle_error(env, status, headers, body, error)
				end
				
				# Generate a suitable response for the given exception.
				# 
				# @parameter exception [Exception] The exception that occurred.
				# @returns [Protocol::HTTP::Response] A response representing the error.
				def failure_response(exception)
					Protocol::HTTP::Response.for_exception(exception)
				end
				
				# Extract protocol information from the environment and response.
				# 
				# @parameter env [Hash] The Rack environment.
				# @parameter response [Protocol::HTTP::Response] The HTTP response.
				# @parameter headers [Hash] The response headers to modify.
				def self.extract_protocol(env, response, headers)
					if protocol = response.protocol
						# This is the newer mechanism for protocol upgrade:
						if env["rack.protocol"]
							headers["rack.protocol"] = protocol
							
						# Older mechanism for protocol upgrade:
						elsif env[CGI::HTTP_UPGRADE]
							headers["upgrade"] = protocol
							headers["connection"] = "upgrade"
						end
					end
				end
			end
		end
	end
end
