package decortsdk

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"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"
)

// BVSDecortClient is HTTP-client for platform
type BVSDecortClient struct {
	client    *http.Client
	cfg       config.BVSConfig
	mutex     *sync.Mutex
	decortURL string
}

type tokenJSON struct {
	AccessToken  string `json:"access_token"`
	TokenType    string `json:"token_type"`
	RefreshToken string `json:"refresh_token"`
	ExpiresIn    uint64 `json:"expires_in"`
}

// Сlient builder
func NewBVS(cfg config.BVSConfig) *BVSDecortClient {
	if err := validators.ValidateConfig(cfg); err != nil {
		panic(validators.ValidationErrors(validators.GetErrors(err)))
	}

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

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

// CloudAPI builder
func (bdc *BVSDecortClient) CloudAPI() *cloudapi.CloudAPI {
	return cloudapi.New(bdc)
}

// CloudBroker builder
func (bdc *BVSDecortClient) CloudBroker() *cloudbroker.CloudBroker {
	return cloudbroker.New(bdc)
}

// SDN builder
func (bdc *BVSDecortClient) SDN() *sdn.SDN {
	return sdn.New(bdc)
}

// DecortApiCall method for sending requests to the platform
func (bdc *BVSDecortClient) DecortApiCall(ctx context.Context, method, url string, params interface{}) ([]byte, error) {
	var body *bytes.Buffer
	var ctype string

	byteSlice, ok := params.([]byte)
	if ok {
		body = bytes.NewBuffer(byteSlice)
		// ctype = "application/x-iso9660-image"
		ctype = "application/octet-stream"
	} else {
		values, err := query.Values(params)
		if err != nil {
			return nil, err
		}
		body = bytes.NewBufferString(values.Encode())
	}

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

	// get token
	if bdc.cfg.Token.AccessToken == "" {
		if _, err = bdc.GetToken(ctx); err != nil {
			return nil, err
		}
	}

	// refresh token
	if bdc.cfg.Token.RefreshToken != "" && bdc.cfg.Token.Expiry.Add(-time.Duration(bdc.cfg.TimeToRefresh)*time.Minute).Before(time.Now()) {
		if _, err := bdc.RefreshToken(ctx); err != nil {
			if _, err = bdc.GetToken(ctx); err != nil {
				return nil, err
			}
		}
	}

	// perform request
	reqCopy := req.Clone(ctx)
	respBytes, err := bdc.do(req, ctype)
	if err == nil {
		return respBytes, nil
	}

	// get token and retry in case of access denied
	if err.Error() == "access is denied" {
		_, err = bdc.GetToken(ctx)
		if err != nil {
			return nil, err
		}

		respBytes, err = bdc.do(reqCopy, "")
		if err != nil {
			return nil, err
		}
	}

	return respBytes, err
}

// DecortApiCallCtype method for sending requests to the platform with content type
func (bdc *BVSDecortClient) DecortApiCallCtype(ctx context.Context, method, url, ctype string, params interface{}) ([]byte, error) {
	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())
	}

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

	// get token
	if bdc.cfg.Token.AccessToken == "" {
		if _, err = bdc.GetToken(ctx); err != nil {
			return nil, err
		}
	}

	// refresh token
	if bdc.cfg.Token.RefreshToken != "" && bdc.cfg.Token.Expiry.Add(-time.Duration(bdc.cfg.TimeToRefresh)*time.Minute).Before(time.Now()) {
		if _, err := bdc.RefreshToken(ctx); err != nil {
			if _, err = bdc.GetToken(ctx); err != nil {
				return nil, err
			}
		}
	}

	// perform request
	reqCopy := req.Clone(ctx)
	respBytes, err := bdc.do(req, ctype)
	if err == nil {
		return respBytes, nil
	}

	// get token and retry in case of access denied
	if err.Error() == "access is denied" {
		_, err = bdc.GetToken(ctx)
		if err != nil {
			return nil, err
		}

		respBytes, err = bdc.do(reqCopy, "")
		if err != nil {
			return nil, err
		}
	}

	return respBytes, err
}

func (bdc *BVSDecortClient) 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, bdc.decortURL+constants.RESTMACHINE+url, body)
	if err != nil {
		return nil, err
	}

	// get token
	if bdc.cfg.Token.AccessToken == "" {
		if _, err = bdc.GetToken(ctx); err != nil {
			return nil, err
		}
	}

	// refresh token
	if bdc.cfg.Token.RefreshToken != "" && bdc.cfg.Token.Expiry.Add(-time.Duration(bdc.cfg.TimeToRefresh)*time.Minute).Before(time.Now()) {
		if _, err := bdc.RefreshToken(ctx); err != nil {
			if _, err = bdc.GetToken(ctx); err != nil {
				return nil, err
			}
		}
	}

	// perform request
	reqCopy := req.Clone(ctx)
	respBytes, err := bdc.do(req, ctype)
	if err == nil {
		return respBytes, nil
	}

	// get token and retry in case of access denied
	if err.Error() == "access is denied" {
		_, err = bdc.GetToken(ctx)
		if err != nil {
			return nil, err
		}

		respBytes, err = bdc.do(reqCopy, ctype)
		if err != nil {
			return nil, err
		}
	}
	return respBytes, err
}

// GetToken allows you to get a token and returns the token structure. When specifying the PathCfg variable,
// the token and configuration will be written to a file.
// When specifying the PathToken variable, the token will be written to a file.
func (bdc *BVSDecortClient) GetToken(ctx context.Context) (config.Token, error) {
	bdc.mutex.Lock()
	defer bdc.mutex.Unlock()

	// set up request headers and body
	body := fmt.Sprintf("grant_type=password&client_id=%s&client_secret=%s&username=%s&password=%s&response_type=token&scope=openid", bdc.cfg.AppID, bdc.cfg.AppSecret, bdc.cfg.Username, bdc.cfg.Password)
	bodyReader := strings.NewReader(body)

	bdc.cfg.SSOURL = strings.TrimSuffix(bdc.cfg.SSOURL, "/")

	req, _ := http.NewRequestWithContext(ctx, "POST", bdc.cfg.SSOURL+"/realms/"+bdc.cfg.Domain+"/protocol/openid-connect/token", bodyReader)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

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

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

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

	// save token in config
	var tj tokenJSON
	if err = json.Unmarshal(tokenBytes, &tj); err != nil {
		return config.Token{}, fmt.Errorf("cannot unmarshal token: %w", err)
	}

	bdc.cfg.Token = config.Token{
		AccessToken:  tj.AccessToken,
		TokenType:    tj.TokenType,
		RefreshToken: tj.RefreshToken,
		Expiry:       tj.expiry(),
	}

	if bdc.cfg.PathCfg != "" {
		ser, _ := bdc.cfg.Serialize("", "    ")
		_ = ser.WriteToFile(bdc.cfg.PathCfg)
	}

	if bdc.cfg.PathToken != "" {
		ser, _ := bdc.cfg.Token.Serialize("", "    ")
		_ = ser.WriteToFile(bdc.cfg.PathToken)
	}

	return bdc.cfg.Token, nil
}

// RefreshToken allows you to refresh a token and returns the token structure. When specifying the PathCfg variable,
// the token and configuration will be written to a file.
// When specifying the PathToken variable, the token will be written to a file
func (bdc *BVSDecortClient) RefreshToken(ctx context.Context) (config.Token, error) {
	bdc.mutex.Lock()
	defer bdc.mutex.Unlock()

	// set up request headers and body
	body := fmt.Sprintf("grant_type=refresh_token&client_id=%s&client_secret=%s&refresh_token=%s&scope=openid", bdc.cfg.AppID, bdc.cfg.AppSecret, bdc.cfg.Token.RefreshToken)
	bodyReader := strings.NewReader(body)

	bdc.cfg.SSOURL = strings.TrimSuffix(bdc.cfg.SSOURL, "/")

	req, _ := http.NewRequestWithContext(ctx, "POST", bdc.cfg.SSOURL+"/realms/"+bdc.cfg.Domain+"/protocol/openid-connect/token", bodyReader)
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

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

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

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

	// save token in config
	var tj tokenJSON
	if err = json.Unmarshal(tokenBytes, &tj); err != nil {
		return config.Token{}, fmt.Errorf("cannot unmarshal after refresh token: %w", err)
	}

	bdc.cfg.Token = config.Token{
		AccessToken:  tj.AccessToken,
		TokenType:    tj.TokenType,
		RefreshToken: tj.RefreshToken,
		Expiry:       tj.expiry(),
	}

	if bdc.cfg.PathCfg != "" {
		ser, _ := bdc.cfg.Serialize("", "    ")
		_ = ser.WriteToFile(bdc.cfg.PathCfg)
	}

	if bdc.cfg.PathToken != "" {
		ser, _ := bdc.cfg.Token.Serialize("", "    ")
		_ = ser.WriteToFile(bdc.cfg.PathToken)
	}

	return bdc.cfg.Token, nil
}

func (e *tokenJSON) expiry() (t time.Time) {
	if v := e.ExpiresIn; v != 0 {
		return time.Now().Add(time.Duration(v) * time.Second)
	}
	return
}

// 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 (bdc *BVSDecortClient) 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.Add("Authorization", "bearer "+bdc.cfg.Token.AccessToken)
	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 := bdc.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 < bdc.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 = bdc.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")
	}

	var respBytes []byte
	respBytes, err = io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	// handle access denied and successful request
	if resp.StatusCode == 401 {
		return respBytes, errors.New("access is denied")
	}
	if resp.StatusCode == 200 {
		return respBytes, nil
	}

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

func trimBVSConfig(cfg *config.BVSConfig) config.BVSConfig {
	cfg.SSOURL = strings.TrimSuffix(cfg.SSOURL, "/")
	cfg.DecortURL = strings.TrimSuffix(cfg.DecortURL, "/")
	return *cfg
}
