package ibclient

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"reflect"
	"strings"
	"time"

	"golang.org/x/net/publicsuffix"
)

type AuthConfig struct {
	Username string
	Password string

	ClientCert []byte
	ClientKey  []byte
}

type HostConfig struct {
	Scheme  string
	Host    string
	Version string
	Port    string
}

type TransportConfig struct {
	SslVerify           bool
	certPool            *x509.CertPool
	HttpRequestTimeout  time.Duration // in seconds
	HttpPoolConnections int
	ProxyUrl            *url.URL
}

func NewTransportConfig(sslVerify string, httpRequestTimeout int, httpPoolConnections int) (cfg TransportConfig) {
	switch {
	case "false" == strings.ToLower(sslVerify):
		cfg.SslVerify = false
	case "true" == strings.ToLower(sslVerify):
		cfg.SslVerify = true
	default:
		caPool := x509.NewCertPool()
		cert, err := ioutil.ReadFile(sslVerify)
		if err != nil {
			log.Printf("Cannot load certificate file '%s'", sslVerify)
			return
		}
		if !caPool.AppendCertsFromPEM(cert) {
			err = fmt.Errorf("cannot append certificate from file '%s'", sslVerify)
			return
		}
		cfg.certPool = caPool
		cfg.SslVerify = true
	}

	cfg.HttpPoolConnections = httpPoolConnections
	cfg.HttpRequestTimeout = time.Duration(httpRequestTimeout)

	return
}

type HttpRequestBuilder interface {
	Init(HostConfig, AuthConfig)
	BuildUrl(r RequestType, objType string, ref string, returnFields []string, queryParams *QueryParams) (urlStr string)
	BuildBody(r RequestType, obj IBObject) (jsonStr []byte)
	BuildRequest(r RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error)
}

type HttpRequestor interface {
	Init(AuthConfig, TransportConfig)
	SendRequest(*http.Request) ([]byte, error)
}

type WapiRequestBuilder struct {
	hostCfg HostConfig
	authCfg AuthConfig
}

type WapiRequestBuilderWithHeaders struct {
	HttpRequestBuilder
	header http.Header
}

func NewWapiRequestBuilderWithHeaders(wrb *WapiRequestBuilder, header http.Header) (*WapiRequestBuilderWithHeaders, error) {
	return &WapiRequestBuilderWithHeaders{
		HttpRequestBuilder: wrb,
		header:             header,
	}, nil
}

func (wrbh *WapiRequestBuilderWithHeaders) BuildRequest(r RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error) {
	req, err = wrbh.HttpRequestBuilder.BuildRequest(r, obj, ref, queryParams)
	if err != nil {
		return req, err
	}
	for h, values := range wrbh.header {
		for _, v := range values {
			req.Header.Add(h, v)
		}
	}
	return req, nil
}

type WapiHttpRequestor struct {
	client http.Client
}

type IBConnector interface {
	CreateObject(obj IBObject) (ref string, err error)
	GetObject(obj IBObject, ref string, queryParams *QueryParams, res interface{}) error
	DeleteObject(ref string) (refRes string, err error)
	UpdateObject(obj IBObject, ref string) (refRes string, err error)
}

type Connector struct {
	hostCfg        HostConfig
	authCfg        AuthConfig
	transportCfg   TransportConfig
	requestBuilder HttpRequestBuilder
	requestor      HttpRequestor
}

type RequestType int

const (
	CREATE RequestType = iota
	GET
	DELETE
	UPDATE
)

func (r RequestType) toMethod() string {
	switch r {
	case CREATE:
		return "POST"
	case GET:
		return "GET"
	case DELETE:
		return "DELETE"
	case UPDATE:
		return "PUT"
	}

	return ""
}

func getHTTPResponseError(resp *http.Response) error {
	defer resp.Body.Close()
	content, _ := ioutil.ReadAll(resp.Body)
	msg := fmt.Sprintf("WAPI request error: %d('%s')\nContents:\n%s\n", resp.StatusCode, resp.Status, content)
	if resp.StatusCode == http.StatusNotFound {
		return NewNotFoundError(msg)
	}
	return errors.New(msg)
}

func (whr *WapiHttpRequestor) Init(authCfg AuthConfig, trCfg TransportConfig) {
	var certList []tls.Certificate

	clientAuthType := tls.NoClientCert

	if authCfg.ClientKey != nil && authCfg.ClientCert != nil {
		cert, err := tls.X509KeyPair(authCfg.ClientCert, authCfg.ClientKey)
		if err != nil {
			log.Fatal("Invalid certificate key pair (PEM format error): ", err)
		}

		certList = []tls.Certificate{cert}
		clientAuthType = tls.RequestClientCert
	}

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			RootCAs:            trCfg.certPool,
			ClientAuth:         clientAuthType,
			Certificates:       certList,
			InsecureSkipVerify: !trCfg.SslVerify,
			Renegotiation:      tls.RenegotiateOnceAsClient,
		},
		MaxIdleConnsPerHost: trCfg.HttpPoolConnections,
		Proxy:               http.ProxyFromEnvironment,
	}

	if trCfg.ProxyUrl != nil {
		tr.Proxy = http.ProxyURL(trCfg.ProxyUrl)
	}

	// All users of cookiejar should import "golang.org/x/net/publicsuffix"
	jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
	if err != nil {
		log.Fatal(err)
	}

	whr.client = http.Client{
		Jar:       jar,
		Transport: tr,
		Timeout:   trCfg.HttpRequestTimeout * time.Second,
	}
}

func (whr *WapiHttpRequestor) SendRequest(req *http.Request) (res []byte, err error) {
	var resp *http.Response
	resp, err = whr.client.Do(req)
	if err != nil {
		return
	} else if !(resp.StatusCode == http.StatusOK ||
		(resp.StatusCode == http.StatusCreated &&
			req.Method == CREATE.toMethod())) {
		err := getHTTPResponseError(resp)
		return nil, err
	}
	defer resp.Body.Close()
	res, err = ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Printf("Http Reponse ioutil.ReadAll() Error: '%s'", err)
		return
	}

	return
}

func NewWapiRequestBuilder(hostCfg HostConfig, authCfg AuthConfig) (*WapiRequestBuilder, error) {
	wrb := WapiRequestBuilder{
		hostCfg: hostCfg,
		authCfg: authCfg,
	}

	return &wrb, nil
}

func (wrb *WapiRequestBuilder) Init(hostCfg HostConfig, authCfg AuthConfig) {
	wrb.hostCfg = hostCfg
	wrb.authCfg = authCfg
}

func (wrb *WapiRequestBuilder) BuildUrl(t RequestType, objType string, ref string, returnFields []string, queryParams *QueryParams) (urlStr string) {
	path := []string{"wapi", "v" + wrb.hostCfg.Version}
	if len(ref) > 0 {
		path = append(path, ref)
	} else {
		path = append(path, objType)
	}

	qry := ""
	vals := url.Values{}
	if t == GET {
		if len(returnFields) > 0 {
			vals.Set("_return_fields", strings.Join(returnFields, ","))
		}
		if queryParams != nil {
			// TODO need to get this from individual objects in future
			if queryParams.forceProxy {
				vals.Set("_proxy_search", "GM")
			}
			for k, v := range queryParams.searchFields {
				if res, ok := ValidateMultiValue(v); ok {
					for _, mv := range res {
						vals.Add(k, strings.TrimSpace(mv))
					}
				} else {
					vals.Set(k, v)
				}
			}
		}

		qry = vals.Encode()
	}

	scheme := "https"
	if wrb.hostCfg.Scheme == "http" {
		scheme = "http"
	}
	u := url.URL{
		Scheme:   scheme,
		Host:     wrb.hostCfg.Host + ":" + wrb.hostCfg.Port,
		Path:     strings.Join(path, "/"),
		RawQuery: qry,
	}

	return u.String()
}

// populateNilLists fills nil lists in IBObject structs, since NIOS doesn't accept a null list in JSON payload.
func (wrb *WapiRequestBuilder) populateNilLists(obj IBObject) IBObject {
	objVal := reflect.ValueOf(obj)
	if reflect.ValueOf(obj).Kind() == reflect.Ptr {
		objVal = objVal.Elem()
	}

	for i := 0; i < objVal.NumField(); i++ {
		fieldVal := objVal.Field(i)
		if fieldVal.Type().Kind() == reflect.Slice && fieldVal.CanSet() {
			if fieldVal.IsNil() == true {
				fieldVal.Set(reflect.MakeSlice(fieldVal.Type(), 0, 0))
			}
		}
	}

	return obj
}

func (wrb *WapiRequestBuilder) BuildBody(t RequestType, obj IBObject) []byte {
	var objJSON []byte
	var err error

	obj = wrb.populateNilLists(obj)

	objJSON, err = json.Marshal(obj)
	if err != nil {
		log.Printf("Cannot marshal object '%s': %s", obj, err)
		return nil
	}

	eaSearch := obj.EaSearch()
	if t == GET && len(eaSearch) > 0 {
		eaSearchJSON, err := json.Marshal(eaSearch)
		if err != nil {
			log.Printf("Cannot marshal EA Search attributes. '%s'\n", err)
			return nil
		}
		objJSON = append(append(objJSON[:len(objJSON)-1], byte(',')), eaSearchJSON[1:]...)
	}

	return objJSON
}

func (wrb *WapiRequestBuilder) BuildRequest(t RequestType, obj IBObject, ref string, queryParams *QueryParams) (req *http.Request, err error) {
	var (
		objType      string
		returnFields []string
	)
	if obj != nil {
		objType = obj.ObjectType()
		returnFields = obj.ReturnFields()
	}
	urlStr := wrb.BuildUrl(t, objType, ref, returnFields, queryParams)

	var bodyStr []byte
	if obj != nil && (t == CREATE || t == UPDATE) {
		bodyStr = wrb.BuildBody(t, obj)
	}

	req, err = http.NewRequest(t.toMethod(), urlStr, bytes.NewBuffer(bodyStr))
	if err != nil {
		log.Printf("cannot build request: '%s'", err)
		return
	}
	req.Header.Set("Content-Type", "application/json")
	if wrb.authCfg.Username != "" {
		req.SetBasicAuth(wrb.authCfg.Username, wrb.authCfg.Password)
	}

	return
}

func (c *Connector) makeRequest(t RequestType, obj IBObject, ref string, queryParams *QueryParams) (res []byte, err error) {
	var req *http.Request
	req, err = c.requestBuilder.BuildRequest(t, obj, ref, queryParams)
	if err != nil {
		return
	}
	res, err = c.requestor.SendRequest(req)
	if err != nil {
		if queryParams != nil {
			/* Forcing the request to redirect to Grid Master by making forcedProxy=true */
			queryParams.forceProxy = true
			req, err = c.requestBuilder.BuildRequest(t, obj, ref, queryParams)
			if err != nil {
				return
			}
			res, err = c.requestor.SendRequest(req)
		} else {
			return nil, err
		}
	}

	return
}

func (c *Connector) CreateObject(obj IBObject) (ref string, err error) {
	ref = ""
	queryParams := NewQueryParams(false, nil)
	resp, err := c.makeRequest(CREATE, obj, "", queryParams)
	if err != nil || len(resp) == 0 {
		log.Printf("CreateObject request error: '%s'\n", err)
		return
	}

	err = json.Unmarshal(resp, &ref)
	if err != nil {
		log.Printf("cannot unmarshall '%s', err: '%s'\n", string(resp), err)
		return
	}

	return
}

func (c *Connector) GetObject(
	obj IBObject, ref string,
	queryParams *QueryParams, res interface{}) (err error) {

	resp, err := c.makeRequest(GET, obj, ref, queryParams)
	if err != nil {
		return
	}
	//to check empty underlying value of interface
	var result interface{}
	err = json.Unmarshal(resp, &result)
	if err != nil {
		log.Printf("cannot unmarshall to check empty value '%s': '%s'\n", string(resp), err)
	}

	var data []interface{}
	if resp == nil || (reflect.TypeOf(result) == reflect.TypeOf(data) && len(result.([]interface{})) == 0) {
		if queryParams == nil {
			err = NewNotFoundError("requested object not found")
			return
		}
		queryParams.forceProxy = true
		resp, err = c.makeRequest(GET, obj, ref, queryParams)
	}
	if err != nil {
		log.Printf("GetObject request error: '%s'\n", err)
	}
	err = json.Unmarshal(resp, res)
	if err != nil {
		log.Printf("cannot unmarshall '%s', err: '%s'\n", string(resp), err)
		return
	}

	if string(resp) == "[]" {
		return NewNotFoundError("not found")
	}

	return
}

func (c *Connector) DeleteObject(ref string) (refRes string, err error) {
	refRes = ""
	queryParams := NewQueryParams(false, nil)
	resp, err := c.makeRequest(DELETE, nil, ref, queryParams)
	if err != nil {
		log.Printf("DeleteObject request error: '%s'\n", err)
		return
	}

	err = json.Unmarshal(resp, &refRes)
	if err != nil {
		log.Printf("cannot unmarshall '%s': '%s'\n", string(resp), err)
		return
	}

	return
}

func (c *Connector) UpdateObject(obj IBObject, ref string) (refRes string, err error) {
	queryParams := NewQueryParams(false, nil)
	refRes = ""
	resp, err := c.makeRequest(UPDATE, obj, ref, queryParams)
	if err != nil {
		log.Printf("failed to update object %s: %s", obj.ObjectType(), err)
		return
	}

	err = json.Unmarshal(resp, &refRes)
	if err != nil {
		log.Printf("cannot unmarshall update object response'%s', err: '%s'\n", string(resp), err)
		return
	}
	return
}

// Logout sends a request to invalidate the ibapauth cookie and should
// be used in a defer statement after the Connector has been successfully
// initialized.
func (c *Connector) Logout() (err error) {
	queryParams := NewQueryParams(false, nil)
	_, err = c.makeRequest(CREATE, nil, "logout", queryParams)
	if err != nil {
		log.Printf("Logout request error: '%s'\n", err)
	}

	return
}

var ValidateConnector = validateConnector

func validateConnector(c *Connector) (err error) {
	// GET UserProfile request is used here to validate connector's basic auth and reachability.
	// TODO: It seems to be broken, needs to be fixed.
	//var response []UserProfile
	//userprofile := NewUserProfile(UserProfile{})
	//err = c.GetObject(userprofile, "", &response)
	//if err != nil {
	//	log.Printf("Failed to connect to the Grid, err: %s \n", err)
	//}
	return
}

func NewConnector(hostConfig HostConfig, authCfg AuthConfig, transportConfig TransportConfig,
	requestBuilder HttpRequestBuilder, requestor HttpRequestor) (res *Connector, err error) {
	res = nil

	connector := &Connector{
		hostCfg:      hostConfig,
		authCfg:      authCfg,
		transportCfg: transportConfig,
	}

	//connector.requestBuilder = WapiRequestBuilder{WaipHostConfig: connector.hostCfg}
	connector.requestBuilder = requestBuilder
	connector.requestBuilder.Init(connector.hostCfg, connector.authCfg)

	connector.requestor = requestor
	connector.requestor.Init(connector.authCfg, connector.transportCfg)

	res = connector
	err = ValidateConnector(connector)
	return
}
