// doReq sends a request, with timeout options and retries, waits for response and returns it
func (c *client) doReq(req *Request, options ...Options) (*Response, errors.Error) {

	if circuitbreaker.Open(req.service, req.endpoint) {
		inst.Counter(1.0, fmt.Sprintf("client.error.%s.%s.circuitbroken", req.service, req.endpoint), 1)
		log.Warnf("Broken Circuit for %s.%s", req.service, req.endpoint)
		return nil, errors.CircuitBroken("com.hailocab.kernel.platform.circuitbreaker", "Circuit is open")
	}

	retries := c.defaults["retries"].(int)
	var timeout time.Duration
	timeoutSupplied := false
	if len(options) == 1 {
		if _, ok := options[0]["retries"]; ok {
			retries = options[0]["retries"].(int)
		}
		if _, ok := options[0]["timeout"]; ok {
			timeout = options[0]["timeout"].(time.Duration)
			timeoutSupplied = true
		}
	}

	// setup the response channel
	rc := make(chan *Response, retries)
	c.responses.add(req, rc)
	defer c.responses.removeByRequest(req)

	instPrefix := fmt.Sprintf("client.%s.%s", req.service, req.endpoint)
	tAllRetries := time.Now()

	for i := 1; i <= retries+1; i++ {
		t := time.Now()

		c.RLock()
		con := c.listening
		c.RUnlock()
		if !con {
			log.Debug("[Client] not yet listening, establishing now...")
			ch := make(chan bool)
			go c.listen(ch)
			if online := <-ch; !online {
				log.Error("[Client] Listener failed")
				inst.Timing(1.0, fmt.Sprintf("%s.error", instPrefix), time.Since(t))
				inst.Counter(1.0, "client.error.com.hailocab.kernel.platform.client.listenfail", 1)
				return nil, errors.InternalServerError("com.hailocab.kernel.platform.client.listenfail", "Listener failed")
			}

			log.Info("[Client] Listener online")
		}

		// figure out what timeout to use
		if !timeoutSupplied {
			timeout = c.timeout.Get(req.service, req.endpoint, i)
		}
		log.Tracef("[Client] Sync request attempt %d for %s using timeout %v", i, req.MessageID(), timeout)

		// only bother sending the request if we are listening, otherwise allow to timeout
		if err := raven.SendRequest(req, c.instanceID); err != nil {
			log.Errorf("[Client] Failed to send request: %v", err)
		}

		select {
		case payload := <-rc:
			if payload.IsError() {
				inst.Timing(1.0, fmt.Sprintf("%s.error", instPrefix), time.Since(t))

				errorProto := &pe.PlatformError{}
				if err := payload.Unmarshal(errorProto); err != nil {
					inst.Counter(1.0, "client.error.com.hailocab.kernel.platform.badresponse", 1)
					return nil, errors.BadResponse("com.hailocab.kernel.platform.badresponse", err.Error())
				}

				err := errors.FromProtobuf(errorProto)
				inst.Counter(1.0, fmt.Sprintf("client.error.%s", err.Code()), 1)

				circuitbreaker.Result(req.service, req.endpoint, err)

				return nil, err
			}

			inst.Timing(1.0, fmt.Sprintf("%s.success", instPrefix), time.Since(t))
			circuitbreaker.Result(req.service, req.endpoint, nil)
			return payload, nil
		case <-time.After(timeout):
			// timeout
			log.Errorf("[Client] Timeout talking to %s.%s after %v for %s", req.Service(), req.Endpoint(), timeout, req.MessageID())
			inst.Timing(1.0, fmt.Sprintf("%s.error", instPrefix), time.Since(t))
			c.traceAttemptTimeout(req, i, timeout)

			circuitbreaker.Result(req.service, req.endpoint, errors.Timeout("com.hailocab.kernel.platform.timeout",
				fmt.Sprintf("Request timed out talking to %s.%s from %s (most recent timeout %v)", req.Service(), req.Endpoint(), req.From(), timeout),
				req.Service(),
				req.Endpoint()))
		}
	}

	inst.Timing(1.0, fmt.Sprintf("%s.error.timedOut", instPrefix), time.Since(tAllRetries))
	inst.Counter(1.0, "client.error.com.hailocab.kernel.platform.timeout", 1)

	return nil, errors.Timeout(
		"com.hailocab.kernel.platform.timeout",
		fmt.Sprintf("Request timed out talking to %s.%s from %s (most recent timeout %v)", req.Service(), req.Endpoint(), req.From(), timeout),
		req.Service(),
		req.Endpoint(),
	)
}
// ConfiguredHttpCaller with more explicit configuration options than simple HttpCaller
func ConfiguredHttpCaller(opts Options) Caller {
	tp := &httpclient.Transport{
		ConnectTimeout:        durationOrDefault(opts.ConnectTimeout, 5*time.Second),
		RequestTimeout:        durationOrDefault(opts.RequestTimeout, 5*time.Second),
		ResponseHeaderTimeout: durationOrDefault(opts.ResponseHeaderTimeout, 5*time.Second),
	}
	if opts.TlsSkipVerify {
		tp.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	}

	httpClient := &http.Client{Transport: tp}

	return func(req *client.Request, rsp proto.Message) errors.Error {
		u, err := url.Parse(opts.BaseUrl)

		q := u.Query()
		q.Set("session_id", req.SessionID())
		q.Set("service", req.Service())
		q.Set("endpoint", req.Endpoint())
		u.Path = "/rpc"
		u.RawQuery = q.Encode()

		var httpReq *http.Request

		// send JSON req content-type to thin API as form-encoded data
		// send proto req content-type directly as bytes, with proto content type
		if req.ContentType() == jsonContentType {
			values := make(url.Values)
			values.Set("service", req.Service())
			values.Set("endpoint", req.Endpoint())
			values.Set("request", string(req.Payload()))
			httpReq, _ = http.NewRequest("POST", u.String(), bytes.NewReader([]byte(values.Encode())))
			httpReq.Header.Set("Content-Type", formEncodedContentType)
		} else {
			httpReq, _ = http.NewRequest("POST", u.String(), bytes.NewReader(req.Payload()))
			httpReq.Header.Set("Content-Type", protoContentType)
		}

		log.Tracef("[Multiclient] HTTP caller - calling '%s' : content-type '%s'", u.String(), req.ContentType())

		httpRsp, err := httpClient.Do(httpReq)
		if err != nil {
			log.Warnf("[Multiclient] HTTP caller error calling %s.%s via %s : %s", req.Service(), req.Endpoint(), u.String(), err)
			return errors.InternalServerError("multiclienthttp.postform", fmt.Sprintf("Error calling %s.%s via %s : %s", req.Service(), req.Endpoint(), u.String(), err))
		}

		defer httpRsp.Body.Close()
		rspBody, err := ioutil.ReadAll(httpRsp.Body)
		if err != nil {
			return errors.BadResponse("multiclienthttp.readresponse", fmt.Sprintf("Error reading response bytes: %v", err))
		}

		// what status code?
		if httpRsp.StatusCode != 200 {
			// deal with error
			e := &protoerror.PlatformError{}
			var err error
			if req.ContentType() == jsonContentType {
				jsonErr := &errorBody{}
				err = json.Unmarshal(rspBody, jsonErr)
				e.Code = proto.String(jsonErr.DottedCode)
				e.Context = jsonErr.Context
				e.Description = proto.String(jsonErr.Payload)
				e.HttpCode = proto.Uint32(uint32(httpRsp.StatusCode))
				// this conversion is lossy, since the JSON response for errors, as crafted
				// by the "thin API", does not currently include the error type, so we have
				// to guess from HTTP status code, but there is no distinct code for "BAD_RESPONSE"
				switch httpRsp.StatusCode {
				case 400:
					e.Type = protoerror.PlatformError_BAD_REQUEST.Enum()
				case 403:
					e.Type = protoerror.PlatformError_FORBIDDEN.Enum()
				case 404:
					e.Type = protoerror.PlatformError_NOT_FOUND.Enum()
				case 500:
					e.Type = protoerror.PlatformError_INTERNAL_SERVER_ERROR.Enum()
				case 504:
					e.Type = protoerror.PlatformError_TIMEOUT.Enum()
				}
			} else {
				err = proto.Unmarshal(rspBody, e)
			}
			// some issue understanding error rsp
			if err != nil {
				return errors.BadResponse("multiclienthttp.unmarshalerr", fmt.Sprintf("Error unmarshaling error response '%s': %v", string(rspBody), err))
			}
			return errors.FromProtobuf(e)
		}

		// unmarshal response
		if req.ContentType() == jsonContentType {
			err = json.Unmarshal(rspBody, rsp)
		} else {
			err = proto.Unmarshal(rspBody, rsp)
		}
		if err != nil {
			return errors.BadResponse("multiclienthttp.unmarshal", fmt.Sprintf("Error unmarshaling response: %v", err))
		}

		return nil
	}
}