package salesforce

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

	"github.com/tidbcloud/account/commons/config"

	"github.com/tidbcloud/account/commons/metrics"

	"github.com/patrickmn/go-cache"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
)

const (
	accessTokenApiPrefix     = "/services/oauth2/token"
	TimeFormat               = "2006-01-02 15:04:05.000Z"
	salesforceAccessTokenKey = "salesforce_access_token"
	httpClientTimeout        = 10 * time.Second
)

type AccessTokenResponse struct {
	AccessToken string `json:"access_token"`
}

type Config struct {
	SalesforceDomain       string
	SalesforceClientID     string
	SalesforceClientSecret string
	SalesforceUsername     string
	SalesforcePassword     string
}

func (c *Client) getAccessToken() (*AccessTokenResponse, error) {
	salesforceConfig := c.cfg

	params := url.Values{}
	params.Add("grant_type", "password")
	params.Add("client_id", salesforceConfig.SalesforceClientID)
	params.Add("client_secret", salesforceConfig.SalesforceClientSecret)
	params.Add("username", salesforceConfig.SalesforceUsername)
	params.Add("password", salesforceConfig.SalesforcePassword)
	loginURL := fmt.Sprintf("https://%s%s?%s", salesforceConfig.SalesforceDomain, accessTokenApiPrefix, params.Encode())
	client := &http.Client{
		Timeout: httpClientTimeout,
		Transport: &http.Transport{
			TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
		},
	}

	res, err := client.Post(loginURL, "application/json", nil)
	if err != nil {
		return nil, err
	}

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

	err = res.Body.Close()
	if err != nil {
		return nil, err
	}

	if res.StatusCode == http.StatusOK {
		tokenResponse := &AccessTokenResponse{}
		err = json.Unmarshal(body, tokenResponse)
		if err != nil {
			return nil, err
		}
		return tokenResponse, nil
	}
	return nil, fmt.Errorf("failed to get salesforce access token. status code: %d, response body: %s", res.StatusCode, string(body[:]))
}

type Client struct {
	sync.Mutex
	baseURL          *url.URL
	httpClient       *http.Client
	accessToken      string
	accessTokenCache *cache.Cache

	cfg *Config
}

func NewClient(accessTokenCache *cache.Cache, cfg *Config) (*Client, error) {
	// disable HTTP2
	httpClient := &http.Client{
		Timeout: httpClientTimeout,
		Transport: &http.Transport{
			TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper),
		},
	}
	client := &Client{
		httpClient:       httpClient,
		accessTokenCache: accessTokenCache,

		cfg: cfg,
	}

	err := client.setBaseURL()
	if err != nil {
		return client, err
	}

	err = client.setAccessToken()
	if err != nil {
		return client, err
	}

	return client, nil
}

func (c *Client) setBaseURL() error {
	apiPath := fmt.Sprintf("https://%s", c.cfg.SalesforceDomain)
	baseURL, err := url.Parse(apiPath)
	if err != nil {
		return err
	}
	c.baseURL = baseURL
	return nil
}

func (c *Client) setAccessToken() error {
	c.Lock()
	defer c.Unlock()
	resp, err := c.getAccessToken()
	if err != nil {
		return err
	}
	accessToken := resp.AccessToken
	c.accessToken = accessToken
	c.accessTokenCache.Set(salesforceAccessTokenKey, accessToken, cache.DefaultExpiration)
	return nil
}

type Error struct {
	body []byte
	resp *http.Response
}

func (e Error) Status() int {
	return e.resp.StatusCode
}

func (e Error) Error() string {
	msg := string(e.body)
	if msg == "" {
		msg = http.StatusText(e.Status())
	}

	return fmt.Sprintf("HTTP %d: %s", e.resp.StatusCode, msg)
}

type Response struct {
	Message string `json:"message"`
	Code    string `json:"code"`
}

func (c *Client) prepareRequest(req *http.Request) *http.Request {
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.accessToken))
	return req
}

func updatePrometheus(statusCode int) {
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", 200)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", 400)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", 401)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", 500)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", 501)).Add(0)
	metrics.ThirdPartyResponse.WithLabelValues("salesforce", fmt.Sprintf("%d", statusCode)).Inc()
}

func (c *Client) post(ctx context.Context, path string, payload interface{}) ([]byte, error) {
	bytes, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	tilog.Info(ctx, "salesforce sync input", zap.String("body", string(bytes)))

	req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s%s", c.baseURL.String(), path), strings.NewReader(string(bytes)))
	if err != nil {
		return nil, err
	}

	req = c.prepareRequest(req)
	var resp *http.Response
	resp, err = c.httpClient.Do(req)
	if err != nil {
		return nil, err
	}

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

	updatePrometheus(resp.StatusCode)
	tilog.Info(ctx, "salesforce sync output", zap.String("url", fmt.Sprintf("%s%s", c.baseURL.String(), path)), zap.Int("status", resp.StatusCode), zap.String("response", string(respBodyBytes)))

	if resp.StatusCode == http.StatusUnauthorized {
		err = c.setAccessToken()
		if err != nil {
			return nil, err
		}
		req = c.prepareRequest(req)
		resp, err = c.httpClient.Do(req)
		if err != nil {
			return nil, err
		}

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

		updatePrometheus(resp.StatusCode)
		tilog.Info(ctx, "salesforce sync after retry with access token", zap.String("url", fmt.Sprintf("%s%s", c.baseURL.String(), path)), zap.Int("status", resp.StatusCode), zap.String("response", string(respBodyBytes)))
	}

	if resp.StatusCode != http.StatusOK {
		return nil, Error{
			body: respBodyBytes,
			resp: resp,
		}
	}
	return respBodyBytes, nil
}

func NewSalesforceClient(centralConfig *config.CentralConfig) (*Client, error) {
	salesforceConfig := centralConfig.SalesforceConfig

	accessTokenCache := cache.New(20*time.Minute, 10*time.Minute)
	salesforceCfg := &Config{
		SalesforceDomain:       salesforceConfig.SalesforceDomain,
		SalesforceClientID:     salesforceConfig.SalesforceClientID,
		SalesforceClientSecret: salesforceConfig.SalesforceClientSecret,
		SalesforceUsername:     salesforceConfig.SalesforceUsername,
		SalesforcePassword:     salesforceConfig.SalesforcePassword,
	}

	return NewClient(accessTokenCache, salesforceCfg)
}
