package decortsdk

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/google/go-querystring/query"
	"repository.basistech.ru/BASIS/decort-golang-sdk/config"
	"repository.basistech.ru/BASIS/decort-golang-sdk/internal/constants"
	"repository.basistech.ru/BASIS/decort-golang-sdk/internal/validators"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudbroker"
	"repository.basistech.ru/BASIS/decort-golang-sdk/pkg/sdn"
)

// LegacyDecortClient is Legacy HTTP-client for platform
type LegacyDecortClient struct {
	decortURL  string
	client     *http.Client
	cfg        config.LegacyConfig
	expiryTime time.Time
	mutex      *sync.Mutex
}

// Legacy client builder
func NewLegacy(cfg config.LegacyConfig) *LegacyDecortClient {
	if err := validators.ValidateConfig(cfg); err != nil {
		panic(validators.ValidationErrors(validators.GetErrors(err)))
	}

	if cfg.Retries == 0 {
		cfg.Retries = 5
	}

	var expiryTime time.Time

	if cfg.Token != "" {
		expiryTime = time.Now().AddDate(0, 0, 1)
	}

	return &LegacyDecortClient{
		decortURL: cfg.DecortURL,
		client: &http.Client{
			Transport: &http.Transport{
				TLSClientConfig: &tls.Config{
					//nolint:gosec
					InsecureSkipVerify: cfg.SSLSkipVerify,
				},
			},
		},
		cfg:        trimLegacyConfig(&cfg),
		expiryTime: expiryTime,
		mutex:      &sync.Mutex{},
	}
}

// CloudAPI builder
func (ldc *LegacyDecortClient) CloudAPI() *cloudapi.CloudAPI {
	return cloudapi.New(ldc)
}

// CloudBroker builder
func (ldc *LegacyDecortClient) CloudBroker() *cloudbroker.CloudBroker {
	return cloudbroker.New(ldc)
}

// SDN builder
func (ldc *LegacyDecortClient) SDN() *sdn.SDN {
	return sdn.New(ldc)
}

// DecortApiCall method for sending requests to the platform
func (ldc *LegacyDecortClient) DecortApiCall(ctx context.Context, method, url string, params interface{}) ([]byte, error) {
	// get token
	if err := ldc.getToken(ctx); err != nil {
		return nil, err
	}

	var body *bytes.Buffer
	var ctype string

	byteSlice, ok := params.([]byte)
	if ok {
		body = bytes.NewBuffer(byteSlice)
		ctype = "application/octet-stream"
	} else {
		values, err := query.Values(params)
		if err != nil {
			return nil, err
		}
		body = bytes.NewBufferString(values.Encode() + fmt.Sprintf("&authkey=%s", ldc.cfg.Token))
	}

	req, err := http.NewRequestWithContext(ctx, method, ldc.decortURL+constants.RESTMACHINE+url, body)
	if err != nil {
		return nil, err
	}

	// perform request
	respBytes, err := ldc.do(req, ctype)
	if err != nil {
		return nil, err
	}

	return respBytes, err
}

// DecortApiCallCtype method for sending requests to the platform with content type
func (ldc *LegacyDecortClient) DecortApiCallCtype(ctx context.Context, method, url, ctype string, params interface{}) ([]byte, error) {
	// get token
	if err := ldc.getToken(ctx); err != nil {
		return nil, err
	}

	var body *bytes.Buffer

	switch ctype {
	case constants.MIMESTREAM:
		body = bytes.NewBuffer(params.([]byte))
	case constants.MIMEJSON:
		jsonBody, err := json.Marshal(params)
		if err != nil {
			return nil, err
		}
		body = bytes.NewBuffer(jsonBody)
	default:
		ctype = constants.MIMEPOSTForm
		values, err := query.Values(params)
		if err != nil {
			return nil, err
		}
		body = bytes.NewBufferString(values.Encode() + fmt.Sprintf("&authkey=%s", ldc.cfg.Token))
	}

	req, err := http.NewRequestWithContext(ctx, method, ldc.decortURL+constants.RESTMACHINE+url, body)
	if err != nil {
		return nil, err
	}

	// perform request
	respBytes, err := ldc.do(req, ctype)
	if err != nil {
		return nil, err
	}

	return respBytes, err
}

func (ldc *LegacyDecortClient) DecortApiCallMP(ctx context.Context, method, url string, params interface{}) ([]byte, error) {
	body, ctype, err := multiPartReq(params)
	if err != nil {
		return nil, err
	}

	req, err := http.NewRequestWithContext(ctx, method, ldc.decortURL+constants.RESTMACHINE+url, body)
	if err != nil {
		return nil, err
	}

	// get token
	if err = ldc.getToken(ctx); err != nil {
		return nil, err
	}

	// perform request
	respBytes, err := ldc.do(req, ctype)
	if err != nil {
		return nil, err
	}

	return respBytes, err
}

func (ldc *LegacyDecortClient) getToken(ctx context.Context) error {
	ldc.mutex.Lock()
	defer ldc.mutex.Unlock()

	// new token is not needed
	if ldc.cfg.Token != "" && !time.Now().After(ldc.expiryTime) {
		return nil
	}

	// set up request headers and body
	body := fmt.Sprintf("username=%s&password=%s", url.QueryEscape(ldc.cfg.Username), url.QueryEscape(ldc.cfg.Password))
	bodyReader := strings.NewReader(body)

	req, _ := http.NewRequestWithContext(ctx, "POST", ldc.cfg.DecortURL+constants.RESTMACHINE+"/cloudapi/user/authenticate", bodyReader)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

	// request token
	resp, err := ldc.client.Do(req)
	if err != nil || resp == nil {
		return fmt.Errorf("cannot get token: %w", err)
	}
	defer resp.Body.Close()

	var tokenBytes []byte
	tokenBytes, err = io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("cannot get token: %w", err)
	}

	if resp.StatusCode != 200 {
		return fmt.Errorf("cannot get token: %s", tokenBytes)
	}

	// save token in config
	token := string(tokenBytes)
	ldc.cfg.Token = token
	ldc.expiryTime = time.Now().AddDate(0, 0, 1)

	return nil
}

// do method performs request and returns response as an array of bytes and nil error in case of response status code 200.
// In any other cases do returns nil response and error.
// Retries are implemented in case of connection reset errors.
func (ldc *LegacyDecortClient) do(req *http.Request, ctype string) ([]byte, error) {
	// set up request headers and body
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	if ctype != "" {
		req.Header.Set("Content-Type", ctype)
	}

	req.Header.Set("Accept", "application/json")

	buf, err := io.ReadAll(req.Body)
	if err != nil {
		return nil, err
	}

	req.Body.Close()
	req.Body = io.NopCloser(bytes.NewBuffer(buf))

	resp, err := ldc.client.Do(req)
	if resp != nil {
		defer resp.Body.Close()
	}

	// retries logic GOES HERE
	// get http response
	//var resp *http.Response
	//for i := uint64(0); i < ldc.cfg.Retries; i++ {
	//	req := req.Clone(req.Context())
	//	req.Body = io.NopCloser(bytes.NewBuffer(buf))
	//
	//	if i > 0 {
	//		time.Sleep(5 * time.Second) // no time sleep for the first request
	//	}
	//
	//	resp, err = ldc.client.Do(req)
	//
	//	// stop retries on success and close response body
	//	if resp != nil {
	//		defer resp.Body.Close()
	//	}
	//	if err == nil {
	//		break
	//	}
	//
	//	// retries in case of connection errors with time sleep
	//	if isConnectionError(err) {
	//		continue
	//	}
	//
	//	// return error in case of non-connection error
	//	return nil, err
	//}

	// handle http request errors
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, fmt.Errorf("got empty response without error")
	}

	// handle successful request
	respBytes, _ := io.ReadAll(resp.Body)
	if resp.StatusCode == 200 {
		return respBytes, nil
	}

	// handle errors with status code other than 200
	err = fmt.Errorf("%s", respBytes)
	return nil, fmt.Errorf("could not execute request: %w", err)
}

func trimLegacyConfig(cfg *config.LegacyConfig) config.LegacyConfig {
	cfg.DecortURL = strings.TrimSuffix(cfg.DecortURL, "/")
	return *cfg
}
