package salesforce

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

	"github.com/patrickmn/go-cache"
	"github.com/pingcap/log"
	"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
	loginURL := fmt.Sprintf("https://%s%s?grant_type=password&client_id=%s&client_secret=%s&username=%s&password=%s", salesforceConfig.SalesforceDomain, accessTokenApiPrefix, salesforceConfig.SalesforceClientID, salesforceConfig.SalesforceClientSecret, salesforceConfig.SalesforceUsername, salesforceConfig.SalesforcePassword)
	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 := io.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("faild 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 (c *Client) post(path string, payload interface{}) ([]byte, error) {
	bytes, err := json.Marshal(payload)
	if err != nil {
		return nil, err
	}

	log.Info("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
	}

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

	log.Info("salesforce sync output", zap.Int("status", resp.StatusCode), zap.String("body", string(body)))

	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
		}

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

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