package salesforce

import (
	"bytes"
	"encoding/csv"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"reflect"
	"strconv"
	"strings"
	"sync"
	"time"

	"gorm.io/gorm"

	"github.com/pingcap/crm-bot/model"
	util "github.com/pingcap/crm-bot/pkg/util"
)

var host = "https://pingcap.my.salesforce.com"

type IObjectData interface {
	GetRecentItem() []RecentItem
	IsDone() bool
	NextURL() string
}

type RecentItem struct {
	Attributes struct {
		Type string `json:"type"`
		URL  string `json:"url"`
	} `json:"attributes"`
	Id               string `json:"Id"`
	Name             string `json:"Name"`
	LastModifiedDate string `json:"LastModifiedDate"`
}

type SalfeForceObjectData struct {
	ObjectDescribe struct {
		Activateable          bool        `json:"activateable"`
		AssociateEntityType   interface{} `json:"associateEntityType"`
		AssociateParentEntity interface{} `json:"associateParentEntity"`
		Createable            bool        `json:"createable"`
		Custom                bool        `json:"custom"`
		CustomSetting         bool        `json:"customSetting"`
		DeepCloneable         bool        `json:"deepCloneable"`
		Deletable             bool        `json:"deletable"`
		DeprecatedAndHidden   bool        `json:"deprecatedAndHidden"`
		FeedEnabled           bool        `json:"feedEnabled"`
		HasSubtypes           bool        `json:"hasSubtypes"`
		IsInterface           bool        `json:"isInterface"`
		IsSubtype             bool        `json:"isSubtype"`
		KeyPrefix             string      `json:"keyPrefix"`
		Label                 string      `json:"label"`
		LabelPlural           string      `json:"labelPlural"`
		Layoutable            bool        `json:"layoutable"`
		Mergeable             bool        `json:"mergeable"`
		MruEnabled            bool        `json:"mruEnabled"`
		Name                  string      `json:"name"`
		Queryable             bool        `json:"queryable"`
		Replicateable         bool        `json:"replicateable"`
		Retrieveable          bool        `json:"retrieveable"`
		Searchable            bool        `json:"searchable"`
		Triggerable           bool        `json:"triggerable"`
		Undeletable           bool        `json:"undeletable"`
		Updateable            bool        `json:"updateable"`
		Urls                  struct {
			CompactLayouts  string `json:"compactLayouts"`
			RowTemplate     string `json:"rowTemplate"`
			ApprovalLayouts string `json:"approvalLayouts"`
			Listviews       string `json:"listviews"`
			Describe        string `json:"describe"`
			QuickActions    string `json:"quickActions"`
			Layouts         string `json:"layouts"`
			Sobject         string `json:"sobject"`
		} `json:"urls"`
	} `json:"objectDescribe"`
	RecentItems []RecentItem `json:"recentItems"`
}

func (d *SalfeForceObjectData) GetRecentItem() []RecentItem {
	return d.RecentItems
}

func (d *SalfeForceObjectData) IsDone() bool {
	return true
}

func (d *SalfeForceObjectData) NextURL() string {
	return ""
}

type SalfeForceObjectQueryData struct {
	TotalSize      int          `json:"totalSize"`
	Done           bool         `json:"done"`
	NextRecordsUrl string       `json:"nextRecordsUrl"`
	RecentItems    []RecentItem `json:"records"`
}

func (d *SalfeForceObjectQueryData) GetRecentItem() []RecentItem {
	return d.RecentItems
}

func (d *SalfeForceObjectQueryData) IsDone() bool {
	return d.Done
}

func (d *SalfeForceObjectQueryData) NextURL() string {
	return d.NextRecordsUrl
}

type AuthData struct {
	AccessToken string `json:"access_token"`
	InstanceURL string `json:"instance_url"`
	ID          string `json:"id"`
	TokenType   string `json:"token_type"`
	IssuedAt    string `json:"issued_at"`
	Signature   string `json:"signature"`
}

func GetAuthToken() (string, error) {
	urlStr := "https://login.salesforce.com/services/oauth2/token"

	data := url.Values{}
	data.Set("grant_type", "password")
	data.Set("client_id", os.Getenv("client_id"))
	data.Set("client_secret", os.Getenv("client_secret"))
	data.Set("username", os.Getenv("username"))
	data.Set("password", os.Getenv("password"))

	header := map[string]string{
		"Content-Type": "application/x-www-form-urlencoded",
	}

	req, err := util.HttpRequest(http.MethodPost, urlStr, header, nil, data)
	if err != nil {
		return "", err
	}

	auth := AuthData{}
	err = util.Do(req, &auth)
	if err != nil {
		return "", err
	}

	return auth.AccessToken, err
}

func GetDelObejctByFullQuery(authToken string, params interface{}, data IObjectData) error {
	url := "/services/data/v52.0/queryAll/"
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s", host, url), header, params, nil)
	if err != nil {
		return err
	}
	err = util.Do(req, data)
	if err != nil {
		return err
	}

	log.Println(req.URL, len(data.GetRecentItem()))
	return err
}

func GetObejctByQuery(authToken string, params interface{}, data IObjectData) error {
	url := "/services/data/v52.0/query/"
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s", host, url), header, params, nil)
	if err != nil {
		return err
	}
	err = util.Do(req, data, util.SetHttpTimeout(time.Minute))
	if err != nil {
		return err
	}

	log.Println(req.URL, len(data.GetRecentItem()))
	return err
}

func GetObejct(authToken string, object interface{}) (IObjectData, error) {
	url := "/services/data/v52.0/sobjects/"
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s%v", host, url, object), header, nil, nil)
	if err != nil {
		return nil, err
	}
	data := SalfeForceObjectData{}
	err = util.Do(req, &data, util.SetHttpTimeout(time.Minute))
	if err != nil {
		return nil, err
	}

	fmt.Println(req.URL, len(data.RecentItems))
	return &data, err
}

func GetObjectDataByData(url, authToken string, data IObjectData) error {
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s", host, url), header, nil, nil)
	if err != nil {
		return err
	}

	err = util.Do(req, data, util.SetHttpTimeout(time.Minute))
	if err != nil {
		return err
	}

	return nil
}

func GetObjectData(url, authToken string) (IObjectData, error) {
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s", host, url), header, nil, nil)
	if err != nil {
		return nil, err
	}

	data := SalfeForceObjectQueryData{}
	err = util.Do(req, &data, util.SetHttpTimeout(time.Minute))
	if err != nil {
		return nil, err
	}

	fmt.Println(req.URL, len(data.RecentItems))
	return &data, err
}

type RowData map[string]interface{}

func GetRow(url, authToken string) (RowData, error) {
	header := map[string]string{
		"Authorization": "Bearer " + authToken,
	}
	req, err := util.HttpRequest(http.MethodGet, fmt.Sprintf("%s%s", host, url), header, nil, nil)
	if err != nil {
		return nil, err
	}

	data := RowData{}
	err = util.Do(req, &data, util.SetHttpTimeout(10*time.Second))
	if err != nil {
		return nil, err
	}

	return data, err
}

type FieldDesc struct {
	FieldLable string
	FieldName  string
	FieldType  string
	FieldObj   string
}

type CurrencyRate struct {
	TotalSize int  `json:"totalSize"`
	Done      bool `json:"done"`
	Records   []struct {
		ID             string  `json:"Id"`
		IsoCode        string  `json:"IsoCode"`
		ConversionRate float64 `json:"ConversionRate"`
	} `json:"records"`
}

func (d *CurrencyRate) GetRecentItem() []RecentItem {
	return nil
}

func (d *CurrencyRate) IsDone() bool {
	return d.Done
}

func (d *CurrencyRate) NextURL() string {
	return ""
}

type OpportunityFieldHistory struct {
	TotalSize      int                                              `json:"totalSize"`
	Done           bool                                             `json:"done"`
	NextRecordsUrl string                                           `json:"nextRecordsUrl"`
	Records        []model.PingcapSalesforceOpportunityFieldHistory `json:"records"`
}

func (d *OpportunityFieldHistory) GetRecentItem() []RecentItem {
	return nil
}

func (d *OpportunityFieldHistory) IsDone() bool {
	return d.Done
}

func (d *OpportunityFieldHistory) NextURL() string {
	return d.NextRecordsUrl
}

func WashData(datas []map[string]interface{}, filterFile, authToken string) []map[string]interface{} {
	csvData, _ := os.ReadFile(filterFile)
	csvReader := csv.NewReader(bytes.NewReader(csvData))
	_, _ = csvReader.Read()
	fieldArrConf := make([]FieldDesc, 0, 10)

	for {
		record, err := csvReader.Read()
		if err != nil {
			break
		}
		fieldArrConf = append(fieldArrConf,
			FieldDesc{
				FieldLable: record[0],
				FieldName:  record[1],
				FieldType:  record[2],
				FieldObj:   record[3],
			})
	}

	//ss := strings.Builder{}
	// headerArr := make([]string, 0, len(fieldArrConf))
	// for _, field := range fieldArrConf {
	// 	headerArr = append(headerArr, strconv.Quote(field.FieldLable))
	// }
	//ss.WriteString(strings.Join(headerArr, ","))
	//ss.WriteString("\n")

	tmpArr := make([]interface{}, 0, len(fieldArrConf))
	output := make([]map[string]interface{}, 0, 10)
	kvs := make(map[string]string)
	for _, data := range datas {
		tmp := make(map[string]interface{})
		tmpArr = tmpArr[:0]
		for _, existField := range fieldArrConf {
			if v, ok := data[existField.FieldName]; ok {
				if v != nil {
					switch vv := v.(type) {
					case string:
						str := vv
						if strings.Contains(existField.FieldType, "Lookup") &&
							existField.FieldObj != "" {
							if val, ok := kvs[str]; ok {
								tmpArr = append(tmpArr, val)
							} else {
								obj := &SalfeForceObjectQueryData{}
								err := GetObejctByQuery(authToken,
									map[string]string{
										"q": fmt.Sprintf("SELECT name from %s where Id = '%s'",
											existField.FieldObj, str)},
									obj)
								if err == nil && len(obj.GetRecentItem()) > 0 {
									kvs[str] = obj.GetRecentItem()[0].Name
									tmpArr = append(tmpArr, obj.GetRecentItem()[0].Name)
								} else {
									tmpArr = append(tmpArr, v)
								}
							}
						} else {
							if strings.Contains(str, "href") {
								begin := strings.IndexByte(str, '>')
								end := strings.LastIndexByte(str, '<')
								str = str[begin+1 : end]
							} else if strings.ContainsAny(str, "<>") {
								begin := strings.Index(str, "<")
								if begin != -1 && strings.Contains(str[begin+1:], ">") {
									str = str[begin+1:]
									end := strings.Index(str, ">")
									str = str[:end]
								}
							}

							if existField.FieldType == "Date" && strings.Contains(str, "/") {
								str = strings.Replace(str, "/", "-", -1)
							}

							tmpArr = append(tmpArr, strings.Replace(str, "\n", "\\n", -1))
						}
					case float64:
						tmpArr = append(tmpArr, vv)
					case bool:
						tmpArr = append(tmpArr, strconv.FormatBool(vv))
					default:
						fmt.Printf("%s is Type = %T\n", existField.FieldLable, vv)
					}

					tmp[existField.FieldLable] = tmpArr[len(tmpArr)-1]
				} else {
					tmpArr = append(tmpArr, "")
				}
			} else {
				tmpArr = append(tmpArr, "")
			}
		}

		//ss.WriteString(strings.Join(tmpArr, ","))
		//ss.WriteString("\n")

		output = append(output, tmp)
	}

	//washedFile := strings.TrimRight(FilterFile, filepath.Ext(FilterFile)) + "_washed.csv"
	//fd, _ := os.OpenFile(washedFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
	//fd.WriteString(ss.String())
	//fd.Close()

	return output
}

type SFTpl struct {
	Object         interface{}
	FilterFile     string
	Fn             func(string, interface{}, IObjectData) error
	Topic          string
	Type           reflect.Type
	LastUpdateTime string
	Delete         bool
	KeyMap         map[string]string
}

type SFTpl1 struct {
	Object           interface{}
	FilterFile       string
	Fn               func(string, interface{}, IObjectData) error
	Topic            string
	Type             reflect.Type
	LastUpdateTime   string
	Delete           bool
	KeyMap           map[string]string
	WidetableObjName string
}

func CrmSqs() {
	//lastUpdateTime := time.Now().Add(-48 * time.Hour).Format(time.RFC3339)
	//if os.Getenv("InitSQS") == "true" {
	//	lastUpdateTime = ""
	//}
	//ss := []SFTpl{
	//	{map[string]string{"q": "SELECT name from Opportunity"},
	//		"./etc/salesforce_opportunity.csv",
	//		GetObejctByQuery,
	//		"pingcap-salesforce-opportunity",
	//		nil,
	//		lastUpdateTime,
	//		false,
	//	},
	//	{map[string]string{"q": "SELECT name from Lead"},
	//		"./etc/salesforce_lead.csv",
	//		GetObejctByQuery,
	//		"pingcap-salesforce-lead",
	//		nil,
	//		lastUpdateTime,
	//		false,
	//	},
	//	{map[string]string{"q": "SELECT name from Order"},
	//		"./etc/salesforce_order.csv",
	//		GetObejctByQuery,
	//		"pingcap-salesforce-order",
	//		nil,
	//		lastUpdateTime,
	//		false,
	//	},
	//}
	//
	//// send to sqs
	//sess := session.Must(session.NewSessionWithOptions(session.Options{
	//	SharedConfigState: session.SharedConfigEnable,
	//}))
	//
	//svc := sqs.New(sess)
	//
	//queue := "ingress-mim.fifo"
	//result, err := svc.GetQueueUrl(&sqs.GetQueueUrlInput{
	//	QueueName: &queue,
	//})
	//
	//if err != nil {
	//	log.Fatal(err)
	//}
	//
	//for i := range ss {
	//	tpl := &ss[i]
	//	washedData := CrmData(tpl)
	//
	//	for _, data := range washedData {
	//		dataBytes, _ := json.Marshal(data)
	//		_, err = svc.SendMessage(&sqs.SendMessageInput{
	//			MessageGroupId:         aws.String(tpl.Topic),
	//			MessageDeduplicationId: aws.String(uuid.NewString()),
	//			MessageBody:            aws.String(string(dataBytes)),
	//			QueueUrl:               result.QueueUrl,
	//		})
	//
	//		if err != nil {
	//			fmt.Println(err)
	//		}
	//	}
	//}
}

func CrmData(tpl *SFTpl) []map[string]interface{} {
	autoToken, err := GetAuthToken()
	if err != nil {
		log.Println(err)
		return nil
	}

	recentItems := []RecentItem{}
	var iObjectData IObjectData
	iObjectData = &SalfeForceObjectQueryData{}
	err = tpl.Fn(autoToken, tpl.Object, iObjectData)
	if err != nil {
		log.Println(err)
		return nil
	}

	for {
		recentItems = append(recentItems, iObjectData.GetRecentItem()...)
		if iObjectData.IsDone() {
			break
		}
		iObjectData, err = GetObjectData(iObjectData.NextURL(), autoToken)
		if err != nil {
			log.Println(err)
			return nil
		}

	}

	if tpl.Delete {
		ret := []map[string]interface{}{}
		log.Println("Need Delete ", len(recentItems))
		for _, item := range recentItems {
			kvs := map[string]interface{}{}
			if val, ok := tpl.KeyMap["Id"]; ok {
				kvs[val] = item.Id
			} else {
				kvs["Id"] = item.Id
			}

			if val, ok := tpl.KeyMap["Name"]; ok {
				kvs[val] = item.Name
			} else {
				kvs["Name"] = item.Name
			}

			ret = append(ret, kvs)
		}
		return ret
	}

	concurrency := util.NewConcurency(50)
	var lock sync.Mutex
	datas := []map[string]interface{}{}
	for i, item := range recentItems {
		tmpItem := item
		if tmpItem.LastModifiedDate <= tpl.LastUpdateTime {
			log.Println(tpl.Topic, " need deal ", i)
			break
		}
		if tpl.LastUpdateTime != "" {
			log.Println("tpl.LastUpdateTime < tmpItem.LastModifiedDate",
				tpl.LastUpdateTime, tmpItem.LastModifiedDate)
		}

		concurrency.Run(func() {
			data, err := GetRow(tmpItem.Attributes.URL, autoToken)
			if err != nil {
				log.Println(err)
				return
			}

			if tpl.LastUpdateTime != "" {
				log.Printf("[info] update data : %+v\n", data)
			}
			lock.Lock()
			datas = append(datas, data)
			lock.Unlock()
		})
	}
	concurrency.Wait()
	if len(recentItems) > 0 {
		tpl.LastUpdateTime = recentItems[0].LastModifiedDate
	}

	washedData := WashData(datas, tpl.FilterFile, autoToken)
	return washedData
}

func GetOpportunityFieldHistory(init bool) ([]model.PingcapSalesforceOpportunityFieldHistory, error) {
	authToken, err := GetAuthToken()
	if err != nil {
		return nil, err
	}

	iData := &OpportunityFieldHistory{}
	params := map[string]string{"q": "SELECT Id,CreatedDate,NewValue,OldValue,OpportunityId,CreatedById,IsDeleted,Field from OpportunityFieldHistory order by CreatedDate Desc"}
	err = GetObejctByQuery(authToken, params, iData)
	if err != nil {
		return nil, err
	}

	historys := []model.PingcapSalesforceOpportunityFieldHistory{}
	for {
		historys = append(historys, iData.Records...)
		if iData.IsDone() {
			break
		}

		err = GetObjectDataByData(iData.NextURL(), authToken, iData)
		if err != nil {
			log.Println(err)
			return nil, err
		}
	}

	return historys, nil
}

func GetCurrencyRate() ([]model.PingcapCrmCurrencyRate, error) {
	authToken, err := GetAuthToken()
	if err != nil {
		return nil, err
	}

	data := CurrencyRate{}
	params := map[string]string{"q": "SELECT Id, isoCode, ConversionRate from CurrencyType"}
	err = GetObejctByQuery(authToken, params, &data)
	if err != nil {
		return nil, err
	}

	rates := []model.PingcapCrmCurrencyRate{}
	for _, record := range data.Records {
		rates = append(rates,
			model.PingcapCrmCurrencyRate{Currency: record.IsoCode, Rate: record.ConversionRate})
	}
	return rates, nil
}

func Do(gdb *gorm.DB, tpls []SFTpl, init bool) {
	defaultCmpTime := time.Now().AddDate(0, 0, -1).Format(time.RFC3339)
	for i := range tpls {
		tpl := &tpls[i]
		if tpl.LastUpdateTime == "" &&
			!tpl.Delete && !init {
			tpl.LastUpdateTime = defaultCmpTime
		}
		data := CrmData(tpl)
		if data == nil || len(data) == 0 {
			continue
		}

		step := 1000
		for idx := 0; idx < len(data); idx += step {
			var dataBytes []byte
			var err error

			if idx+step >= len(data) {
				dataBytes, _ = json.Marshal(data[idx:])
			} else {
				dataBytes, _ = json.Marshal(data[idx : idx+step])
			}

			sliT := reflect.SliceOf(tpl.Type)
			SliceValue := reflect.New(sliT).Interface()
			err = json.Unmarshal(dataBytes, SliceValue)
			if err != nil {
				log.Println("[error] json unmarshal ", err)
				continue
			}
			// SliceValue is a pointer, so need Elem()
			newData := reflect.ValueOf(SliceValue).Elem().Convert(sliT).Interface()

			if tpl.Delete {
				err = gdb.Delete(newData).Error
			} else {
				err = gdb.Debug().Save(newData).Error
			}

			if err != nil {
				log.Println("db err", err)
			}
		}
	}

	rates, err := GetCurrencyRate()
	if err != nil {
		log.Println("[error] GetCurrencyRate", err)
		return
	}
	gdb.Save(rates)

	historys, err := GetOpportunityFieldHistory(init)
	if err != nil {
		log.Println("[error] GetOpportunityFieldHistory", err)
		return
	}

	err = gdb.Where("1=1").Delete(&model.PingcapSalesforceOpportunityFieldHistory{}).Error
	if err != nil {
		log.Println("[error] delete table failed", model.PingcapSalesforceOpportunityFieldHistory{}.TableName())
	}

	gdb.Save(historys)
}
