/**
 * Copyright 2016 IBM Corp.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package session

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"reflect"
	"strconv"
	"strings"
	"time"

	"github.com/softlayer/softlayer-go/datatypes"
	"github.com/softlayer/softlayer-go/sl"
)

type RestTransport struct{}

// DoRequest - Implementation of the TransportHandler interface for handling
// calls to the REST endpoint.
func (r *RestTransport) DoRequest(sess *Session, service string, method string, args []interface{}, options *sl.Options, pResult interface{}) error {
	restMethod := httpMethod(method, args)

	// Parse any method parameters and determine the HTTP method
	var parameters []byte
	if len(args) > 0 {
		// parse the parameters
		parameters, _ = json.Marshal(
			map[string]interface{}{
				"parameters": args,
			})
	}

	path := buildPath(service, method, options)

	resp, code, err := sendHTTPRequest(sess, path, restMethod, parameters, options)

	if err != nil {
		//Preserve the original sl error
		if _, ok := err.(sl.Error); ok {
			return err
		}
		return sl.Error{Wrapped: err, StatusCode: code}
	}

	err = findResponseError(code, resp)
	if err != nil {
		return err
	}

	// Some APIs that normally return a collection, omit the []'s when the API returns a single value
	returnType := reflect.TypeOf(pResult).String()
	if strings.Index(returnType, "[]") == 1 && strings.Index(string(resp), "[") != 0 {
		resp = []byte("[" + string(resp) + "]")
	}

	// At this point, all that's left to do is parse the return value to the appropriate type, and return
	// any parse errors (or nil if successful)

	err = nil
	switch pResult.(type) {
	case *[]uint8:
		// exclude quotes
		*pResult.(*[]uint8) = resp[1 : len(resp)-1]
	case *datatypes.Void:
	case *uint:
		var val uint64
		val, err = strconv.ParseUint(string(resp), 0, 64)
		if err == nil {
			*pResult.(*uint) = uint(val)
		}
	case *bool:
		*pResult.(*bool), err = strconv.ParseBool(string(resp))
	case *string:
		str := string(resp)
		strIdx := len(str) - 1
		if str == "null" {
			str = ""
		} else if str[0] == '"' && str[strIdx] == '"' {
			rawStr := rawString{str}
			err = json.Unmarshal([]byte(`{"val":`+str+`}`), &rawStr)
			if err == nil {
				str = rawStr.Val
			}
		}
		*pResult.(*string) = str
	default:
		// Must be a json representation of one of the many softlayer datatypes
		err = json.Unmarshal(resp, pResult)
	}

	if err != nil {
		err = sl.Error{Message: err.Error(), Wrapped: err}
	}

	return err
}

type rawString struct {
	Val string
}

func buildPath(service string, method string, options *sl.Options) string {
	path := service

	if options.Id != nil {
		path = path + "/" + strconv.Itoa(*options.Id)
	}

	// omit the API method name if the method represents one of the basic REST methods
	if method != "getObject" && method != "deleteObject" && method != "createObject" &&
		method != "editObject" && method != "editObjects" {
		path = path + "/" + method
	}

	return path + ".json"
}

func encodeQuery(opts *sl.Options) string {
	query := new(url.URL).Query()

	if opts.Mask != "" {
		query.Add("objectMask", opts.Mask)
	}

	if opts.Filter != "" {
		query.Add("objectFilter", opts.Filter)
	}

	// resultLimit=<offset>,<limit>
	// If offset unspecified, default to 0
	if opts.Limit != nil {
		startOffset := 0
		if opts.Offset != nil {
			startOffset = *opts.Offset
		}

		query.Add("resultLimit", fmt.Sprintf("%d,%d", startOffset, *opts.Limit))
	}

	return query.Encode()
}

func sendHTTPRequest(
	sess *Session, path string, requestType string,
	requestBody []byte, options *sl.Options) ([]byte, int, error) {

	retries := sess.Retries
	if retries < 2 {
		return makeHTTPRequest(sess, path, requestType, requestBody, options)
	}

	wait := sess.RetryWait
	if wait == 0 {
		wait = DefaultRetryWait
	}

	return tryHTTPRequest(retries, wait, sess, path, requestType, requestBody, options)
}

func tryHTTPRequest(
	retries int, wait time.Duration, sess *Session,
	path string, requestType string, requestBody []byte,
	options *sl.Options) ([]byte, int, error) {

	resp, code, err := makeHTTPRequest(sess, path, requestType, requestBody, options)
	if err != nil {
		if !isRetryable(err) {
			return resp, code, err
		}

		if retries--; retries > 0 {
			jitter := time.Duration(rand.Int63n(int64(wait)))
			wait = wait + jitter/2
			time.Sleep(wait)
			return tryHTTPRequest(
				retries, wait, sess, path, requestType, requestBody, options)
		}
	}

	return resp, code, err
}

func makeHTTPRequest(
	session *Session, path string, requestType string,
	requestBody []byte, options *sl.Options) ([]byte, int, error) {
	log := Logger

	client := session.HTTPClient
	if client == nil {
		client = &http.Client{}
	}

	client.Timeout = DefaultTimeout
	if session.Timeout != 0 {
		client.Timeout = session.Timeout
	}

	var url string
	if session.Endpoint == "" {
		url = url + DefaultEndpoint
	} else {
		url = url + session.Endpoint
	}
	// fmt.Printf("Calling %s/%s", strings.TrimRight(url, "/"), path)
	url = fmt.Sprintf("%s/%s", strings.TrimRight(url, "/"), path)
	req, err := http.NewRequest(requestType, url, bytes.NewBuffer(requestBody))
	if err != nil {
		return nil, 0, err
	}

	switch {
	case session.APIKey != "":
		req.SetBasicAuth(session.UserName, session.APIKey)
	case session.AuthToken != "":
		req.SetBasicAuth(fmt.Sprintf("%d", session.UserId), session.AuthToken)
	case session.IAMToken != "":
		req.Header.Set("Authorization", session.IAMToken)
	}

	// For cases where session is built from the raw structure and not using New() , the UserAgent would be empty
	if session.userAgent == "" {
		session.userAgent = getDefaultUserAgent()
	}

	req.Header.Set("User-Agent", session.userAgent)

	if session.Headers != nil {
		for key, value := range session.Headers {
			req.Header.Set(key, value)
		}
	}

	req.URL.RawQuery = encodeQuery(options)

	if session.Debug {
		log.Println("[DEBUG] Request URL: ", requestType, req.URL)
		log.Println("[DEBUG] Parameters: ", string(requestBody))
	}

	// Apply custom context.Context, if supplied
	if session.Context != nil {
		req = req.WithContext(session.Context)
	}

	resp, err := client.Do(req)
	if err != nil {
		statusCode := 520
		if resp != nil && resp.StatusCode != 0 {
			statusCode = resp.StatusCode
		}

		if isTimeout(err) {
			statusCode = 599
		}

		return nil, statusCode, err
	}

	defer resp.Body.Close()

	responseBody, err := ioutil.ReadAll(resp.Body)

	if err != nil {
		return nil, resp.StatusCode, err
	}
	if resp.Header["Softlayer-Total-Items"] != nil && len(resp.Header["Softlayer-Total-Items"]) == 1 {
		var str_err error
		var total_items int
		total_items, str_err = strconv.Atoi(resp.Header["Softlayer-Total-Items"][0])
		if str_err != nil {
			log.Println("[Error] Unable to convert Softlayer-Total-Items to int: ", str_err)
		}
		options.SetTotalItems(total_items)
	}

	if session.Debug {
		log.Println("[DEBUG] Status Code: ", resp.StatusCode)
		log.Println("[DEBUG] Response: ", string(responseBody))
	}
	err = findResponseError(resp.StatusCode, responseBody)
	return responseBody, resp.StatusCode, err
}

func httpMethod(name string, args []interface{}) string {
	if name == "deleteObject" {
		return "DELETE"
	} else if name == "editObject" || name == "editObjects" {
		return "PUT"
	} else if name == "createObject" || name == "createObjects" || len(args) > 0 {
		return "POST"
	}

	return "GET"
}

func findResponseError(code int, resp []byte) error {
	if code < 200 || code > 299 {
		e := sl.Error{StatusCode: code}
		err := json.Unmarshal(resp, &e)
		// If unparseable, wrap the json error
		if err != nil {
			e.Wrapped = err
			e.Message = err.Error()
		}
		return e
	}
	// Edge case error detection
	if len(resp) == 0 {
		return sl.Error{
			StatusCode: code,
			Exception:  "SoftLayer_Exception_Public",
			Message:    "Empty Response",
		}
	}
	errorString := `{"error":"Internal Error","code":"SoftLayer_Exception_Public"}`
	if errorString == string(resp) {
		return sl.Error{
			StatusCode: code,
			Exception:  "SoftLayer_Exception_Public",
			Message:    "Internal Error",
		}
	}
	return nil
}
