package utils

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"io"
	"io/ioutil"
	"mime/multipart"
	"net/http"
	"net/url"
	"reflect"
	"strings"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/pingcap/log"
	"github.com/pingcap/tep/pkg/apis/apiserver/v1alpha1"
	"go.uber.org/zap"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

const (
	TEPKindStatus   = "Status"
	TEPKindResponse = "TEPResponse"
)

// HTTP Server utilities

type GetParameterFunc func(name string) string
type GetParameterWithDefault func(string, string) string

// APIStatus is the status of an API response, it implements Error() function, so
// can also be used as general errors
type APIStatus struct {
	httpCode int
	message  string
}

func NewAPIStatus(code int, msg string) APIStatus {
	return APIStatus{httpCode: code, message: msg}
}

func NewOKAPIStatus() APIStatus {
	return APIStatus{httpCode: http.StatusOK, message: "OK"}
}

func (status *APIStatus) HTTPCode() int {
	return status.httpCode
}

func (status *APIStatus) Message() string {
	return status.message
}

func (status *APIStatus) Error() string {
	return fmt.Sprintf("%d: %s", status.httpCode, status.message)
}

func (status *APIStatus) OK() bool {
	return status.httpCode >= 100 && status.httpCode < 400
}

// getHttpParameter get the parameter based on getParameterFunc and trim off the space of parameter value
func GetHTTPParameter(gpf GetParameterFunc, name string) string {
	return strings.TrimSpace(gpf(name))
}

func GetHTTPParameterDefault(gpf GetParameterWithDefault, name string, defaultValue string) string {
	return strings.TrimSpace(gpf(name, defaultValue))
}

func SendMsg(resp *gin.Context, respData interface{}, status APIStatus) {
	log.Debug("sending message", zap.Any("data", respData), zap.Any("status", status))
	if !status.OK() {
		SendErrMsg(resp, respData, status.HTTPCode(), status.Message())
	} else {
		SendOKMsg(resp, respData, status.HTTPCode())
	}
}

// realSendMsg sends response data back to http client
func realSendMsg(resp *gin.Context, respData interface{}, code int, msg string) {
	if respData == nil || reflect.ValueOf(respData).IsNil() {
		resp.JSON(code, &metav1.Status{
			TypeMeta: metav1.TypeMeta{
				APIVersion: v1alpha1.SchemeGroupVersion.String(),
				Kind:       TEPKindStatus,
			},
			Message: msg,
			Code:    int32(code),
		})
		return
	}
	resp.JSON(code, respData)
}

func SendErrMsg(resp *gin.Context, respData interface{}, code int, msg string) {
	realSendMsg(resp, respData, code, msg)
}

func SendOKMsg(resp *gin.Context, respData interface{}, code int) {
	realSendMsg(resp, respData, code, "")
}

// HTTP Client utilities

// HTTPClient is a wrap of http.Client to fit our usage
type HTTPClient struct {
	URL     string
	client  *http.Client
	timeout time.Duration
}

func NewHTTPClient(url string, tlsConfig *tls.Config, timeout ...time.Duration) *HTTPClient {
	client := &HTTPClient{
		URL:     url,
		timeout: time.Second * 10, // default timeout is 10s
		client: &http.Client{
			Transport: &http.Transport{TLSClientConfig: tlsConfig},
		},
	}

	if len(timeout) > 0 {
		client.timeout = timeout[0]
	}

	return client
}

func (c *HTTPClient) SetURL(url string) {
	c.URL = url
	return
}

// Do is a wrap of http.Client.Do(), that send the request unchanged to server
func (c *HTTPClient) Do(req *http.Request) (*http.Response, error) {
	return c.client.Do(req)
}

// Delete build a DELETE request and send it to server
func (c *HTTPClient) Delete() (*http.Response, error) {
	req, err := http.NewRequest("DELETE", c.URL, nil)
	if err != nil {
		return nil, err
	}

	return c.client.Do(req)
}

// Get is a wrap of http.Client.Get(), that send the GET request to server
func (c *HTTPClient) Get() (*http.Response, error) {
	return c.client.Get(c.URL)
}

// Head is a wrap of http.Client.Head(), that send the HEAD request to server
func (c *HTTPClient) Head() (*http.Response, error) {
	return c.client.Head(c.URL)
}

// Patch build a PATCH request and send it to server
func (c *HTTPClient) Patch(body io.Reader) (*http.Response, error) {
	req, err := http.NewRequest("PATCH", c.URL, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	return c.client.Do(req)
}

// Post build a POST request and send it to server
func (c *HTTPClient) Post(body io.Reader) (*http.Response, error) {
	return c.client.Post(c.URL, "application/json", body)
}

// PostForm is a wrap of http.Client.PostForm(), that send the POST request with form to server
func (c *HTTPClient) PostForm(data url.Values) (*http.Response, error) {
	return c.client.PostForm(c.URL, data)
}

// Put build a PUT request and send it to server
func (c *HTTPClient) Put(body io.Reader) (*http.Response, error) {
	req, err := http.NewRequest("PUT", c.URL, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	return c.client.Do(req)
}

func DecodeResponse(resp *http.Response) (map[string]interface{}, error) {
	bodyBytes, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	var body map[string]interface{}
	err = json.Unmarshal(bodyBytes, &body)
	if err != nil {
		return nil, err
	}

	return body, nil
}

func ParseHTTPError(err error) int {
	// http.Client.Do() return *url.Error on errors
	doErr := err.(*url.Error)
	if doErr.Temporary() {
		return http.StatusServiceUnavailable
	}
	if doErr.Timeout() {
		return http.StatusGatewayTimeout
	}
	return http.StatusBadGateway
}

// Other HTTP helper functions and utilities

const UserAttributeName = "user"

// ReadCSV read CSV file from HTTP multipart file and returns a reader
func ReadCSV(fileHeader *multipart.FileHeader) (*csv.Reader, error) {
	file, err := fileHeader.Open()
	if err != nil {
		return nil, err
	}
	defer CallDeferMethod(file.Close, "close file failed")
	return csv.NewReader(file), nil
}

// ReadCACerts try to read trusted CA cert pool from system and optional custom file path
func ReadCACerts(extCAFile string) (*x509.CertPool, error) {
	// try to load system CA certs
	rootCAs, err := x509.SystemCertPool()
	if err != nil {
		return nil, err
	}
	if rootCAs == nil {
		rootCAs = x509.NewCertPool()
	}

	// load external CA cert
	if extCAFile == "" {
		log.Info("External CA file path not set, using system pool only.")
		return rootCAs, nil
	}
	caCert, err := ioutil.ReadFile(extCAFile)
	if err != nil {
		return nil, fmt.Errorf("fail to read CA file %s, error: %v", extCAFile, err)
	}
	if ok := rootCAs.AppendCertsFromPEM(caCert); !ok {
		log.Warn("fail to append CA file to pool, using system CAs only.")
	}
	return rootCAs, nil
}

func GetTLSConfig(caFilePath, certFilePath, keyFilePath string, insecureSkipVerify bool) (*tls.Config, error) {
	tlsConfig := &tls.Config{}
	tlsConfig.InsecureSkipVerify = insecureSkipVerify

	rootCAs, err := ReadCACerts(caFilePath)
	if err != nil {
		return tlsConfig, err
	}
	certPem, err := ioutil.ReadFile(certFilePath)
	if err != nil {
		return tlsConfig, err
	}
	keyPem, err := ioutil.ReadFile(keyFilePath)
	if err != nil {
		return tlsConfig, err
	}

	cert, err := tls.X509KeyPair(certPem, keyPem)
	if err != nil {
		return tlsConfig, err
	}

	tlsConfig.RootCAs = rootCAs
	tlsConfig.Certificates = []tls.Certificate{cert}
	return tlsConfig, nil
}

func CallDeferMethod(f func() error, errMsg string) {
	if err := f(); err != nil {
		log.Error(errMsg, zap.Error(err))
	}
}

type Request interface {
	// Attribute returns the value associated to the given name. Returns nil if absent.
	Get(name string) (interface{}, bool)
}

func GetUsername(request Request) string {
	v, exist := request.Get(UserAttributeName)
	if !exist {
		log.Warn("can not found the user attribute")
		return ""
	}
	return v.(string)
}
