package client

import (
	"crypto/tls"
	"crypto/x509"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
)

const (
	httpHeaderJSON        = "application/json"
	httpHeaderContentType = "Content-Type"
	httpHeaderAccept      = "Accept"
)

// APIClientConfig : Keep config options for APIClient
type APIClientConfig struct {
	Username string
	Password string
	CaFile   string
	CertFile string
	KeyFile  string
	Proxy    string
}

// APIClient provided the http client for trigger http requests
type APIClient struct {
	// http client
	client *http.Client

	// Configuration
	config APIClientConfig
}

// NewAPIClient is constructor of APIClient
func NewAPIClient(config APIClientConfig) (*APIClient, error) {
	// Load client cert
	cert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile)
	if err != nil {
		return nil, err
	}

	// Add ca
	caCert, err := ioutil.ReadFile(config.CaFile)
	if err != nil {
		return nil, err
	}

	caCertPool := x509.NewCertPool()
	caCertPool.AppendCertsFromPEM(caCert)

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{cert},
		RootCAs:      caCertPool,
	}

	tlsConfig.BuildNameToCertificate()
	transport := &http.Transport{
		TLSClientConfig: tlsConfig,
	}

	// If proxy should be set
	if len(strings.TrimSpace(config.Proxy)) > 0 {
		if proxyURL, err := url.Parse(config.Proxy); err == nil {
			transport.Proxy = http.ProxyURL(proxyURL)
		}
	}

	client := &http.Client{
		Transport: transport,
	}

	return &APIClient{
		client: client,
		config: config,
	}, nil

}

// Get data
func (ac *APIClient) Get(url string) ([]byte, error) {
	if strings.TrimSpace(url) == "" {
		return nil, errors.New("empty url")
	}

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return nil, err
	}

	req.Header.Set(httpHeaderAccept, httpHeaderJSON)
	req.SetBasicAuth(ac.config.Username, ac.config.Password)

	resp, err := ac.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer func() {
		if resp.Body != nil {
			resp.Body.Close()
		}
	}()
	if resp.StatusCode != http.StatusOK {
		return nil, errors.New(resp.Status)
	}

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

	return data, nil
}

// Post data
func (ac *APIClient) Post(url string, data []byte) error {
	if strings.TrimSpace(url) == "" {
		return errors.New("Empty url")
	}

	req, err := http.NewRequest("POST", url, strings.NewReader(string(data)))
	if err != nil {
		return err
	}

	req.Header.Set(httpHeaderContentType, httpHeaderJSON)
	req.SetBasicAuth(ac.config.Username, ac.config.Password)
	resp, err := ac.client.Do(req)
	if err != nil {
		return err
	}

	if resp.StatusCode != http.StatusCreated &&
		resp.StatusCode != http.StatusOK {
		if err := getErrorMessage(resp); err != nil {
			return fmt.Errorf("%s:%s", resp.Status, err.Error())
		}

		return errors.New(resp.Status)
	}

	return nil
}

// Delete data
func (ac *APIClient) Delete(url string) error {
	if strings.TrimSpace(url) == "" {
		return errors.New("Empty url")
	}

	req, err := http.NewRequest("DELETE", url, nil)
	if err != nil {
		return err
	}

	req.Header.Set(httpHeaderAccept, httpHeaderJSON)
	req.SetBasicAuth(ac.config.Username, ac.config.Password)

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

	if resp.StatusCode != http.StatusOK {
		if err := getErrorMessage(resp); err != nil {
			return fmt.Errorf("%s:%s", resp.Status, err.Error())
		}

		return errors.New(resp.Status)
	}

	return nil
}

// SwitchAccount : Switch account
func (ac *APIClient) SwitchAccount(username, password string) {
	if len(strings.TrimSpace(username)) == 0 ||
		len(strings.TrimSpace(password)) == 0 {
		return
	}

	ac.config.Username = username
	ac.config.Password = password
}

// Read error message from response body
func getErrorMessage(resp *http.Response) error {
	if resp == nil {
		return errors.New("nil response")
	}

	if resp.Body == nil || resp.ContentLength == 0 {
		// nothing to read
		return nil
	}

	defer resp.Body.Close()

	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		// abandon to read deatiled error message
		return nil
	}

	return fmt.Errorf("%s", data)
}
