package restapi

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
)

type HttpClientFactory func() *http.Client

// BaseAPIResponse represents the most basic standard Centrify API response,
//	where Result itself is left as raw json
type BaseAPIResponse struct {
	Success bool `json:"success"`
	Result  json.RawMessage
	Message string
}

type StringResponse struct {
	BaseAPIResponse
	Result string
}

type BoolResponse struct {
	BaseAPIResponse
	Result bool
}

// GenericMapResponse represents Centrify API responses where results are map[string]interface{},
//	this type allows direct access to these without further decoding.
type GenericMapResponse struct {
	BaseAPIResponse
	Result map[string]interface{}
}

// BackendType is the type of backend that is being implemented
type RestClientMode uint32

// RestClient represents a stateful API client (cookies maintained between calls, single service etc)
type RestClient struct {
	Service      string
	Client       *http.Client
	Headers      map[string]string
	SourceHeader string
}

// GetNewRestClient creates a new RestClient for the specified endpoint.  If a factory for creating
//	http.Client's is not provided, you'll get a new: &http.Client{}.
func GetNewRestClient(service string, httpFactory HttpClientFactory) (*RestClient, error) {
	jar, err := cookiejar.New(nil)

	if err != nil {
		return nil, err
	}

	// Munge on the service a little bit, force it to have no trailing / and always start with https://
	url, err := url.Parse(service)
	if err != nil {
		return nil, err
	}
	url.Scheme = "https"
	url.Path = ""

	client := &RestClient{}
	client.Service = url.String()
	if httpFactory != nil {
		client.Client = httpFactory()
	} else {
		client.Client = &http.Client{}
	}
	client.Client.Jar = jar
	client.Headers = make(map[string]string)
	client.SourceHeader = "cloud-golang-sdk"
	return client, nil
}

func (r *RestClient) CallRawAPI(method string, args map[string]interface{}) ([]byte, error) {
	return r.postAndGetBody(method, args)
}

func (r *RestClient) CallBaseAPI(method string, args map[string]interface{}) (*BaseAPIResponse, error) {
	body, err := r.postAndGetBody(method, args)
	if err != nil {
		return nil, err
	}
	return bodyToBaseAPIResponse(body)
}

func (r *RestClient) CallGenericMapAPI(method string, args map[string]interface{}) (*GenericMapResponse, error) {
	body, err := r.postAndGetBody(method, args)
	if err != nil {
		return nil, err
	}
	return bodyToGenericMapResponse(body)
}

func (r *RestClient) CallStringAPI(method string, args map[string]interface{}) (*StringResponse, error) {
	body, err := r.postAndGetBody(method, args)
	if err != nil {
		return nil, err
	}
	return bodyToStringResponse(body)
}

func (r *RestClient) CallBoolAPI(method string, args map[string]interface{}) (*BoolResponse, error) {
	body, err := r.postAndGetBody(method, args)
	if err != nil {
		return nil, err
	}
	return bodyToBoolResponse(body)
}

func (r *RestClient) postAndGetBody(method string, args map[string]interface{}) ([]byte, error) {
	service := strings.TrimSuffix(r.Service, "/")
	method = strings.TrimPrefix(method, "/")
	postdata := strings.NewReader(payloadFromMap(args))
	postreq, err := http.NewRequest("POST", service+"/"+method, postdata)

	if err != nil {
		return nil, err
	}

	postreq.Header.Add("Content-Type", "application/json")
	postreq.Header.Add("X-CENTRIFY-NATIVE-CLIENT", "Yes")
	postreq.Header.Add("X-CFY-SRC", r.SourceHeader)

	for k, v := range r.Headers {
		postreq.Header.Add(k, v)
	}

	httpresp, err := r.Client.Do(postreq)
	if err != nil {
		return nil, err
	}

	defer httpresp.Body.Close()

	if httpresp.StatusCode == 200 {
		return ioutil.ReadAll(httpresp.Body)
	}

	body, _ := ioutil.ReadAll(httpresp.Body)
	return nil, fmt.Errorf("POST to %s failed with code %d, body: %s", method, httpresp.StatusCode, body)
}

// This function converts a map[string]interface{} into json string
func payloadFromMap(input map[string]interface{}) string {
	if input != nil {
		p, _ := json.Marshal(input)
		return string(p)
	}

	return ""
}

func bodyToBaseAPIResponse(body []byte) (*BaseAPIResponse, error) {
	reply := &BaseAPIResponse{}
	err := json.Unmarshal(body, &reply)
	if err != nil {
		return nil, fmt.Errorf("Failed to unmarshal BaseApiResponse from HTTP response: %v", err)
	}
	return reply, nil
}

func bodyToGenericMapResponse(body []byte) (*GenericMapResponse, error) {
	reply := &GenericMapResponse{}
	err := json.Unmarshal(body, &reply)
	if err != nil {
		return nil, fmt.Errorf("Failed to unmarshal GenericMapResponse from HTTP response: %v", err)
	}
	return reply, nil
}

func bodyToStringResponse(body []byte) (*StringResponse, error) {
	reply := &StringResponse{}
	err := json.Unmarshal(body, &reply)
	if err != nil {
		return nil, fmt.Errorf("Failed to unmarshal StringResponse from HTTP response: %v", err)
	}
	return reply, nil
}

func bodyToBoolResponse(body []byte) (*BoolResponse, error) {
	reply := &BoolResponse{}
	err := json.Unmarshal(body, &reply)
	if err != nil {
		return nil, fmt.Errorf("Failed to unmarshal BoolResponse from HTTP response: %v", err)
	}
	return reply, nil
}
