/**
 * 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/base64"
	"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{}

const IBMCLOUDIAMENDPOINT = "https://iam.cloud.ibm.com/identity/token"

//IAMTokenResponse ...
type IAMTokenResponse struct {
	AccessToken  string `json:"access_token"`
	RefreshToken string `json:"refresh_token"`
	TokenType    string `json:"token_type"`
}

// IAMErrorMessage -
type IAMErrorMessage struct {
	ErrorMessage string `json:"errormessage"`
	ErrorCode    string `json:"errorcode"`
}

// 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 code == 500 && (sess.IAMToken != "" && sess.IAMRefreshToken != "") {
			authErr := refreshToken(sess)
			if authErr == nil {
				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)
				}
			}
			if authErr != nil {
				return resp, code, fmt.Errorf("Unable to refresh auth token: {{%v}}", authErr)
			}

		}
		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
	}
	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 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
	}
	return nil
}

func refreshToken(sess *Session) error {

	client := http.DefaultClient
	reqPayload := url.Values{}
	reqPayload.Add("grant_type", "refresh_token")
	reqPayload.Add("refresh_token", sess.IAMRefreshToken)

	req, err := http.NewRequest("POST", IBMCLOUDIAMENDPOINT, strings.NewReader(reqPayload.Encode()))
	if err != nil {
		return err
	}
	req.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("bx:bx")))
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	req.Header.Add("Accept", "application/json")
	var token IAMTokenResponse
	var eresp IAMErrorMessage

	resp, err := client.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()

	responseBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	if resp != nil && resp.StatusCode != 200 {
		err = json.Unmarshal(responseBody, &eresp)
		if err != nil {
			return err
		}
		if eresp.ErrorCode != "" {
			return sl.Error{Exception: eresp.ErrorCode, Message: eresp.ErrorMessage}
		}
	}

	err = json.Unmarshal(responseBody, &token)
	if err != nil {
		return err
	}
	sess.IAMToken = fmt.Sprintf("%s %s", token.TokenType, token.AccessToken)
	sess.IAMRefreshToken = token.RefreshToken
	return nil
}
