require 'openid/errors'
require 'openid/constants'
require 'openid/util'

require 'net/http'
require 'uri'
require 'open-uri'

module OpenID
	
	def quote_minimal(s)
		# TODO: implement? (Used by normalize_url's unicode handling in the python modules)
		return s
	end
	# Strip whitespace off, and add http:// if http:// or https:// is not already
	# present.
	def normalize_url(url)
		url.strip!
		if (!url.index(/^http:\/\/|^https:\/\//))
			url = 'http://' + url
		end
		
		# TODO: Some unicode handling
		# (Keeping in mind that ruby's unicode/string distinction is kinda nil so far)
		
		return url
	end
	
	# Provides a very simple interface to get from and post to http servers
	class SimpleHTTPClient
		# Returns the the url which was retrieved, and the retrived data
		# (data.base_uri, data)
		def get(url)
			uri = URI.parse(url)
			begin
				data = uri.read
			ensure
				if data
					return data.base_uri, data
				else
					return nil, nil
				end
			end
		end
		# Takes the url to post body to.
		# Returns the url retrieved, and the body of the response received.
		def post(url, body)
			uri = URI.parse(url)
			response = nil
			Net::HTTP.start(uri.host, uri.port) { |http|
				response = http.post(uri.request_uri(), body)
			}
			# TODO: some error checking here
			# TODO: return actually retrieved url
			return url, response.body
		end
		
	end #class SimpleHTTPClient
	
	class Consumer
		include OpenID
		# Takes an http_client and an association_manager. Will create some automatically if none are passed in.
		def initialize(http_client = SimpleHTTPClient.new(), association_manager = DumbAssociationManager.new())
			@http_client = http_client
			@association_manager = association_manager
		end
		# Returns the url to redirect to or nil if no identity is found
		def handle_request(url, return_to, trust_root = nil, immediate = false)
			url = normalize_url(url)
			
			server_info = find_server(url)
			return nil if server_info == nil
			identity, server_url = server_info
			redir_args = { 'openid.identity' => identity, 'openid.return_to' => return_to}
			
			redir_args['openid.trust_root'] = trust_root if trust_root
			
			if immediate
				mode = 'checkid_immediate'
			else
				mode = 'checkid_setup'
			end
			
			redir_args['openid.mode'] = mode
			assoc_handle = @association_manager.associate(server_url)
			if assoc_handle
				redir_args['openid.assoc_handle'] = assoc_handle
			end
			
			return append_args(server_url, redir_args).to_s
		end
		# Handles an OpenID GET request with openid.mode in the arguments. req should
		# be a Request instance, properly initialized with the http arguments given,
		# and the http method used to make the request. Returns the expiry time of
		# the session as a Time.
		#
		# Will raise a ProtocolError if the http_method is not GET, or the request
		# mode is unknown.
		def handle_response(req)
			if req.http_method != 'GET'
				raise ProtocolError, "Expected HTTP Method 'GET', got #{req.http_method}", caller
			end
			begin
				return __send__('do_' + req['mode'], req)
			rescue NoMethodError
				raise ProtocolError, "Unknown Mode: #{req['mode']}", caller
			end
		end
		
		def determine_server_url(req)
			identity, server_url = find_server(req['identity'])
			if req['identity'] != identity
				raise ValueMismatchError, "ID URL #{req['identity']} seems to have moved: #{identity}", caller
			end
			return server_url
		end
		
		# Returns identity_url, server_url or nil if no server is found.
		def find_server(url)
			identity, data = @http_client.get(url)
			identity = identity.to_s
			server = nil
			delegate = nil
			parse_link_attrs(data) { |link|
				rel = link['rel']
				if rel == 'openid.server' and server == nil
					href = link['href']
					server = href if href
				end
				if rel == 'openid.delegate' and delegate == nil
					href = link['href']
					delegate = href if href
				end
			}
			return nil if !server
			identity = delegate if delegate
			return normalize_url(identity), normalize_url(server)
		end
		
		def _dumb_auth(server_url, now, req)
			if !verify_return_to(req)
				raise ValueMismatchError, "return_to is not valid", caller
			end
			check_args = {}
			req.args.each { |k, v| check_args[k] = v if k.index('openid.') == 0 }
			check_args['openid.mode'] = 'check_authentication'
			body = url_encode(check_args)
			url, data = @http_client.post(server_url, body)
			results = parse_kv(data)
			lifetime = results['lifetime'].to_i
			if lifetime
				invalidate_handle = results['invalidate_handle']
				if invalidate_handle
					@association_manager.invalidate(server_url, invalidate_handle)
				end
				return now + lifetime
			else
				raise ValueMismatchError, 'Server failed to validate signature', caller
			end
		end
		
		def do_id_res(req)
			now = Time.now
			user_setup_url = req.get('user_setup_url')
			raise UserSetupNeeded, user_setup_url, caller if user_setup_url
			server_url = determine_server_url(req)
			assoc = @association_manager.get_association(server_url, req['assoc_handle'])
			if assoc == nil
				return _dumb_auth(server_url, now, req)
			end
			sig = req.get('sig')
			signed_fields = req.get('signed').strip.split(',')
			signed, v_sig = sign_reply(req.args, assoc.secret, signed_fields)
			if v_sig != sig
				raise ValueMismatchError, "Signatures did not match: #{req.args}, #{v_sig}, #{assoc.secret}", caller
			end
			issued = DateTime.strptime(req.get('issued')).to_time
			valid_to = [assoc.expiry, DateTime.strptime(req.get('valid_to')).to_time].min
			return now + (valid_to - issued)
		end
		# Handle an error from the server
		def do_error(req)
			error = req.get('error')
			if error
				raise ProtocolError, "Server Response: #{error}", caller
			else
				raise ProtocolError, "Unspecified Server Error: #{req.args}", caller
			end
		end
		
		def do_cancel(req)
			raise UserCancelled
		end
		
		# This is called before the consumer makes a check_authentication call to the
		# server. It can be used to verify that the request being authenticated
		# is valid by confirming that the openid.return_to value signed by the server
		# corresponds to this consumer. The full OpenID::Request object is passed in.
		# Should return true if the return_to field corresponds to this consumer, 
		# false otherwise. The default function performs no check and returns true.
		def verify_return_to(req)
			return true	
		end	
	end #class Consumer
		
		
end #module OpenID