package v1

import (
	"bytes"
	"context"
	"encoding/base64"
	"encoding/json"
	"io"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
	"github.com/pingcap-inc/pchub/pkg/rstorage"
	"github.com/spf13/viper"

	"github.com/pingcap-inc/security/internal/alert-server/store"
	v1 "github.com/pingcap-inc/security/model/v1"
)

type dunsService struct {
	store store.Factory
}

type DunsSrv interface {
	DunCheck(ctx context.Context) error
}

var _ DunsSrv = (*dunsService)(nil)

func newDuns(str *service) *dunsService {
	return &dunsService{store: str.store}
}

const (
	dunsBaseUrl = "https://plus.dnb.com"
	base64key   = "base64:"
	duns        = "duns-"
	dunsToken   = "dunsToken"
	dunsProb    = "dunsProblem-"
	dunsIDR     = "dunsIDR-"
)

// 全局锁
var dunsLock sync.Mutex

func (ds *dunsService) DunCheck(ctx context.Context) error {
	log.L(ctx).Info("DunCheck Called.")

	// 保证只有一个线程在执行
	dunsLock.Lock()
	defer dunsLock.Unlock()

	dtLockToken, err := GetDunsToken(ctx, 0)
	if err != nil {
		return err
	}
	drPlusToken, err := GetDunsToken(ctx, 1)
	if err != nil {
		return err
	}

	log.L(ctx).Infof("dtLockToken:%s,drPlusToken:%s", dtLockToken, drPlusToken)

	records, err := ds.store.Company().ListCompanyByStatus(ctx, 0)
	if err != nil {
		log.L(ctx).Errorf("list error: %v", err)

		return err
	}

	// 多线程处理
	wg := &sync.WaitGroup{}
	wgChan := make(chan struct{}, 10)

	for _, rd := range records {
		wgChan <- struct{}{}
		wg.Add(1)

		go func(rd *v1.Company) {
			defer func() {
				<-wgChan
				wg.Done()
			}()

			if rd.Status != 0 {
				return
			}
			rd.Status = 1

			if strings.ReplaceAll(rd.Name, " ", "") == "" {
				rd.Status = InvalidCompany
				_ = ds.store.Company().UpdateCompanyStatus(ctx, rd)

				return
			}

			// 制裁信息
			comInfos, err := getComProInfo(ctx, rd, drPlusToken)
			if err != nil {
				log.L(ctx).Errorf("get company info error: %v", err)

				rd.Status = 2
			}

			riMap := make(map[string]int)
			sanCount := 0

			for _, comInfo := range comInfos {
				err = ds.store.Company().SaveComProblem(ctx, comInfo)
				if err != nil && !IsDuplicate(err) {
					log.L(ctx).Errorf("save company info error: %v", err)

					rd.Status = 2
				}

				comSanction, err := GetCompanySanctions(ctx, comInfo.EntityID, drPlusToken)
				if err != nil {
					log.L(ctx).Errorf("get company sanction error: %v", err)

					rd.Status = 2
				}

				err = ds.SaveSanctions(ctx, *rd, comInfo, comSanction)
				if err != nil && !IsDuplicate(err) {
					log.L(ctx).Errorf("get company sanction error: %v", err)

					rd.Status = 2
				}

				// save problem info
				icons := strings.Split(comInfo.RiskIcons, ",")
				for _, icon := range icons {
					if icon != "" {
						riMap[icon]++
					}
				}

				if comSanction != nil {
					for _, sanc := range comSanction.MatchedProfile {
						if len(sanc.WatchListContent.DataProviders) != 0 {
							sanCount++
						}
					}
				}
			}

			// 公司基本信息
			if strings.ReplaceAll(rd.Country, " ", "") != "" {
				basics, err := GetCompanyDunsByIDR(ctx, rd, dtLockToken)
				if err != nil {
					log.L(ctx).Errorf("get company others error: %v", err)

					rd.Status = 2
				}

				for _, basic := range basics {
					err = ds.store.Company().SaveCompanyBasic(ctx, basic)

					if err != nil && !IsDuplicate(err) {
						log.L(ctx).Errorf("save company others error: %v", err)

						rd.Status = 2
					}
				}

				if len(basics) == 0 && len(comInfos) == 0 && rd.Status != 2 {
					rd.Status = -1
				}
			}

			icons := ""
			for icon, _ := range riMap {
				icons = icons + icon + ","
			}

			rd.RiskIcons = strings.TrimSuffix(icons, ",")
			rd.SancCount = sanCount
			rd.BannedCountry = isBanned(rd.Country)

			err = ds.store.Company().UpdateCompanyStatus(ctx, rd)
			if err != nil {
				log.L(ctx).Errorf("update company status error: %v", err)

				rd.Status = 2
			}

			if rd.Status == 1 {
				log.L(ctx).Infof("search company information success: %s", rd.Name)
			}

			// 重新加载不通知
			if rd.IsReload != 1 {
				// 写入飞书表格
				rds := make([]*v1.Company, 0)
				errTag := false
				if rd.Status == 1 {
					rds = append(rds, rd)
				}
				if rd.Status == 2 {
					errTag = true
				}

				if len(rds) != 0 {
					tok, err := comSrv.GetLarkToken(ctx, dataSafe)
					if err != nil {
						return
					}

					csSrv := &companySearchService{store: ds.store}

					err = csSrv.OutToBitableAddOnly(ctx, tok, rds)
					if err != nil {
						return
					}
				}

				if errTag {
					sdMsg(ctx, errors.New("search company information error: "+rd.Name))
				}
			}
		}(rd)
	}

	wg.Wait()

	log.L(ctx).Info("DunCheck Finished.")

	return nil
}

func (ds *dunsService) SaveSanctions(ctx context.Context, record v1.Company, cmPro *v1.CompanyProblem, comSanction *v1.CompanySanctionResp) error {
	log.L(ctx).Info("SaveSanctions Called.")

	for _, sanction := range comSanction.MatchedProfile {
		organizationNames := ""
		for _, orgName := range sanction.OrganizationNames {
			organizationNames += orgName.NameType + ":" + orgName.Name + ","
		}
		dataProvider := ""
		for _, provider := range sanction.WatchListContent.DataProviders {
			dataProvider += provider.SourceName + ","
		}
		sanc := &v1.CompanySanction{
			CreatedAt:              time.Now(),
			CompanyID:              record.ID,
			CompanyName:            record.Name,
			PrimaryName:            cmPro.PrimaryName,
			EntityID:               cmPro.EntityID,
			OrganizationNames:      strings.TrimSuffix(organizationNames, ","),
			WatchListContentNotes:  sanction.WatchListContent.Notes,
			WatchListContentStatus: sanction.WatchListContent.Status,
			DataProviders:          strings.TrimSuffix(dataProvider, ","),
		}
		if sanc.WatchListContentStatus == "" {
			sanc.WatchListContentStatus = "Unknown"
		}

		sanctionID, err := ds.store.Company().SaveSanction(ctx, sanc)
		if err != nil && !IsDuplicate(err) {
			log.L(ctx).Errorf("save company sanction error: %v", err)

			return err
		}

		sanctionReferences := make([]v1.CompanySanctionReferences, 0)

		for _, reference := range sanction.WatchListContent.SanctionReferences {
			startDate := strconv.Itoa(reference.StartYear) + "-" + strconv.Itoa(reference.StartMonth) + "-" + strconv.Itoa(reference.StartDay)
			endDate := strconv.Itoa(reference.EndYear) + "-" + strconv.Itoa(reference.EndMonth) + "-" + strconv.Itoa(reference.EndDay)
			sanctionReferences = append(sanctionReferences, v1.CompanySanctionReferences{
				CreatedAt:    time.Now(),
				CompanyID:    record.ID,
				CompanyName:  record.Name,
				PrimaryName:  cmPro.PrimaryName,
				SanctionID:   sanctionID,
				Reference:    reference.Reference,
				ProviderName: reference.ProviderName,
				ProviderCode: reference.ProviderCode,
				Description:  reference.Description,
				Status:       reference.Status,
				StartDate:    startDate,
				EndDate:      endDate,
			})
		}
		err = ds.store.Company().SaveSanctionReferences(ctx, sanctionReferences)
		if err != nil {
			log.L(ctx).Errorf("save company sanction references error: %v", err)

			return err
		}
	}

	log.L(ctx).Info("SaveSanctions Finished.")

	return nil
}

func getComProInfo(ctx context.Context, company *v1.Company, token string) ([]*v1.CompanyProblem, error) {
	log.L(ctx).Infof("getComProInfo Called:%s", company.Name)

	// handle quota
	over, err := checkOverQuota(ctx, dunsProb)
	if err != nil {
		return nil, err
	}
	if over {
		return nil, nil
	}

	url := "/v1/screening/dowjones/search"
	name := replaceWithSpace(company.Name)

	params := map[string]interface{}{
		"searchType":      "name",
		"name":            name,
		"categories":      "WL,SOC,AM,TC",
		"entityType":      "E",
		"searchPrecision": "precise",
	}

	var resp []byte

	// 失败重试3次
	for i := 0; i < 3; i++ {
		resp, err = DoDunsRequest(ctx, "GET", url, nil, params, token)
		if err != nil {
			log.L(ctx).Errorf("post error: %v", err)
		} else {
			break
		}

		time.Sleep(time.Second * 2)
	}

	if err != nil {
		return nil, err
	}

	if resp == nil {
		return nil, nil
	}

	var dunsResp v1.CompanyInfoResp
	err = json.Unmarshal(resp, &dunsResp)
	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	if dunsResp.Error.ErrorCode != "" {
		if strings.Contains(dunsResp.Error.ErrorMessage, "No Match found") {
			return nil, nil
		}
		log.L(ctx).Errorf("duns error: %v", dunsResp.Error)

		return nil, errors.New(dunsResp.Error.ErrorMessage)
	}

	comProInfos := make([]*v1.CompanyProblem, 0)

	for index, candidate := range dunsResp.SearchCandidate {
		if index > 15 {
			log.L(ctx).Error("company problem info more than 15")

			break
		}

		riskIcons := ""
		for _, riskIcon := range candidate.RiskIcon {
			riskIcons = riskIcons + riskIcon.Code + ","
		}
		comInfo := &v1.CompanyProblem{
			CreatedAt:            time.Now(),
			CompanyID:            company.ID,
			CompanyName:          company.Name,
			PrimaryName:          candidate.PrimaryName,
			CountryISOAlpha2Code: candidate.CountryISOAlpha2Code,
			EntityType:           candidate.EntityType,
			EntityID:             candidate.EntityID,
			Title:                candidate.Title,
			RiskIcons:            riskIcons,
			MatchedName:          candidate.MatchedNames.Name,
			MatchedNameType:      candidate.MatchedNames.NameType,
			MatchScore:           candidate.MatchQuality.MatchScore,
		}

		comProInfos = append(comProInfos, comInfo)
	}

	log.L(ctx).Infof("getComProInfo Finished:%s", company.Name)

	return comProInfos, nil
}

func GetCompanySanctions(ctx context.Context, entityID, token string) (*v1.CompanySanctionResp, error) {
	url := "/v1/screening/dowjones/data/" + entityID

	params := map[string]interface{}{
		"adverseMediaArticleType": "directUrl",
		"includePrimaryName":      "true",
	}

	var resp []byte
	var err error
	// 失败重试3次
	for i := 0; i < 3; i++ {
		resp, err = DoDunsRequest(ctx, "GET", url, nil, params, token)

		if err != nil {
			log.L(ctx).Errorf("post error: %v", err)
		} else {
			break
		}

		time.Sleep(time.Second * 2)
	}

	if err != nil {
		return nil, err
	}

	if resp == nil {
		return nil, nil
	}

	var dunsResp v1.CompanySanctionResp
	err = json.Unmarshal(resp, &dunsResp)
	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	if len(dunsResp.MatchedProfile) == 0 {
		return nil, nil
	}

	return &dunsResp, nil
}

func GetCompanyDunsByIDR(ctx context.Context, com *v1.Company, token string) ([]*v1.CompanyBasic, error) {
	log.L(ctx).Infof("GetCompanyDunsByIDR Called:%s", com.Name)

	// handle quota
	over, err := checkOverQuota(ctx, dunsIDR)
	if err != nil {
		return nil, err
	}
	if over {
		return nil, nil
	}

	url := "/v1/match/cleanseMatch"

	params := map[string]interface{}{
		"countryISOAlpha2Code": com.Country,
		"name":                 com.Name,
		"inLanguage":           "auto",
	}

	// 失败重试3次
	var resp []byte
	for i := 0; i < 3; i++ {
		resp, err = DoDunsRequest(ctx, "GET", url, nil, params, token)
		if err != nil {
			log.L(ctx).Errorf("post error: %v", err)
		} else {
			break
		}

		time.Sleep(time.Second * 2)
	}

	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, nil
	}

	var dunsResp v1.IDRResp
	err = json.Unmarshal(resp, &dunsResp)

	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	basicInfos := make([]*v1.CompanyBasic, 0)
	for index, mc := range dunsResp.MatchCandidate {
		if index > 15 {
			log.L(ctx).Error("company basic info more than 15")

			break
		}

		address := mc.Organization.PrimaryAddress.AddressCountry.Name + "," + mc.Organization.PrimaryAddress.AddressRegion.Name + "," + mc.Organization.PrimaryAddress.StreetAddress.Line1
		regNums := ""
		for _, regNum := range mc.Organization.RegistrationNumbers {
			regNums = regNums + regNum.RegistrationNumber + ","
		}
		basicInfo := &v1.CompanyBasic{
			CreatedAt:   time.Now(),
			CompanyName: com.Name,
			CompanyID:   com.ID,
			PrimaryName: mc.Organization.PrimaryName,
			Country:     mc.Organization.PrimaryAddress.AddressCountry.IsoAlpha2Code,
			Status:      mc.Organization.DunsControlStatus.OperatingStatus.Description,
			Address:     address,
			RegNumber:   regNums,
			DunsNumber:  mc.Organization.Duns,
		}

		basicInfos = append(basicInfos, basicInfo)
	}

	log.L(ctx).Infof("GetCompanyDunsByIDR Finished:%s", com.Name)

	return basicInfos, nil
}

func GetCompanyInfoAndPrincipals(ctx context.Context, com *v1.CompanyBasic, token string) (*v1.CompanyInfoPrincipalReq, error) {
	log.L(ctx).Infof("GetCompanyInfoAndPrincipals Called:%s %s", com.CompanyName, com.DunsNumber)

	url := "/v1/data/duns/" + com.DunsNumber + "?blockIDs=companyinfo_L2_v1,principalscontacts_L2_v2"

	// 失败重试3次

	var resp []byte
	var err error
	for i := 0; i < 3; i++ {
		resp, err = DoDunsRequest(ctx, "GET", url, nil, nil, token)
		if err != nil {
			log.L(ctx).Errorf("get company info and principals error: %v", err)
		} else {
			break
		}

		time.Sleep(time.Second * 2)
	}

	if err != nil {
		return nil, err
	}

	if resp == nil {
		return nil, nil
	}
	var companyInfo v1.CompanyInfoPrincipalReq
	err = json.Unmarshal(resp, &companyInfo)
	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	return &companyInfo, nil
}

func GetCompanyOthers(ctx context.Context, record v1.Company, dunsNumber, token string) (*v1.CompanyBasic, error) {
	url := "/v1/data/duns/" + dunsNumber

	params := map[string]interface{}{
		"blockIDs": "companyinfo_L2_v1",
	}

	resp, err := DoDunsRequest(ctx, "GET", url, nil, params, token)
	if err != nil {
		return nil, err
	}

	if resp == nil {
		return nil, nil
	}

	var dunsResp v1.CompanyOthersResp
	err = json.Unmarshal(resp, &dunsResp)
	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	return nil, nil
}

type DunsTokenBody struct {
	GrantType string `json:"grant_type"`
}

func GetDunsToken(ctx context.Context, typ int) (string, error) {
	rStore := &rstorage.RedisCluster{
		KeyPrefix: duns,
	}

	var tok string
	val, err := rStore.GetKey(dunsToken + strconv.Itoa(typ))
	if err != nil {
		if !errors.As(err, &rstorage.ErrKeyNotFound) {
			log.L(ctx).Error(errors.Wrap(err, "error when retrieve key in redis").Error())
		}
	}

	tok = val

	if tok == "" {

		// 失败重试3次
		for i := 0; i < 3; i++ {

			url := "/v2/token"

			keyStr := viper.GetString("duns.data-block.key") + ":" + viper.GetString("duns.data-block.secret")
			// base64
			if typ == 1 {
				keyStr = viper.GetString("duns.direct-plus.key") + ":" + viper.GetString("duns.direct-plus.secret")
			}
			key := base64.StdEncoding.EncodeToString([]byte(keyStr))

			body := &DunsTokenBody{
				GrantType: "client_credentials",
			}
			marshal, _ := json.Marshal(body)

			var resp []byte
			resp, err = DoDunsRequest(ctx, "POST", url, bytes.NewReader(marshal), nil, base64key+key)
			if err != nil {
				log.L(ctx).Errorf("post error: %v", err)
			}

			var dunsResp v1.DunsTokenResp
			err = json.Unmarshal(resp, &dunsResp)
			if err != nil {
				log.L(ctx).Errorf("unmarshal error: %v", err)

				return "", err
			}

			tok = dunsResp.AccessToken

			if err = rStore.SetKey(dunsToken+strconv.Itoa(typ), tok, time.Hour*12); err != nil {
				log.L(ctx).Errorf("Error when store multi-sa token into redis: %s", err.Error())
			}

			if tok != "" {
				break
			}

			time.Sleep(time.Second * 2)
		}

		if tok == "" {
			return "", err
		}
	}

	return tok, nil
}

func DoDunsRequest(ctx context.Context, m, url string, body io.Reader, params map[string]interface{}, token string) ([]byte, error) {

	client := &http.Client{}
	req, err := http.NewRequest(m, dunsBaseUrl+url, body)
	if err != nil {
		log.L(ctx).Errorf("new request error: %v", err)

		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")

	if strings.HasPrefix(token, base64key) {
		req.Header.Set("Authorization", "Basic "+strings.TrimPrefix(token, base64key))
	} else {
		req.Header.Set("Authorization", "Bearer "+token)
	}

	query := req.URL.Query()
	if params != nil || len(params) != 0 {
		for k, v := range params {
			query.Set(k, v.(string))
		}
	}

	req.URL.RawQuery = query.Encode()

	resp, err := client.Do(req)

	if err != nil {
		log.L(ctx).Errorf("do request error: %v", err)

		return nil, err
	}

	defer resp.Body.Close()

	respBody, err := io.ReadAll(resp.Body)

	if err != nil {
		log.L(ctx).Errorf("read response error: %v", err)

		return nil, err
	}
	var dunsResp v1.DunsTokenResp
	err = json.Unmarshal(respBody, &dunsResp)
	if err != nil {
		log.L(ctx).Errorf("unmarshal error: %v", err)

		return nil, err
	}

	if dunsResp.Error.ErrorCode != "" {
		log.L(ctx).Errorf("duns error: %v", dunsResp.Error)

		if strings.Contains(dunsResp.Error.ErrorMessage, "No Match found") {
			return nil, nil
		}

		return nil, errors.New(dunsResp.Error.ErrorMessage)
	}

	return respBody, nil
}

func replaceWithSpace(s string) string {
	r1 := regexp.MustCompile(`[&/\\（）【】\-·《》：。]`)
	rmCN := r1.ReplaceAllString(s, " ")
	r2 := regexp.MustCompile(`[&.,'\-!+@#$/()%*]`)
	rmEN := r2.ReplaceAllString(rmCN, " ")
	return rmEN
}

func IsDuplicate(err error) bool {
	return strings.Contains(err.Error(), "Duplicate entry")
}

func checkOverQuota(ctx context.Context, key string) (bool, error) {
	ct, err := getSearchCount(ctx, key)
	if err != nil {
		return false, err
	}

	if ct.Count > 100 {
		log.L(ctx).Errorf("api count more than 100")

		ct := TextMessage{
			Text: "api count more than 100",
		}

		marshal, _ := json.Marshal(ct)

		_, err := SendLarkMsg(ctx, "user_id", MessageBody{
			MsgType:   "text",
			Content:   string(marshal),
			ReceiveID: "9c7ca79f",
		})
		if err != nil {
			return false, err
		}

		return true, nil
	}

	err = setCount(ctx, key, *ct)
	if err != nil {
		return false, err
	}

	return false, nil
}

type countType struct {
	Count int       `json:"count"`
	Time  time.Time `json:"time"`
}

func getSearchCount(ctx context.Context, key string) (*countType, error) {
	log.L(ctx).Info("getSearchCount Called.")

	rStore := &rstorage.RedisCluster{
		KeyPrefix: duns,
	}

	val, err := rStore.GetKey(key)
	if err != nil {
		if !errors.As(err, &rstorage.ErrKeyNotFound) {
			log.L(ctx).Error(errors.Wrap(err, "error when retrieve key in redis").Error())
		}
	}

	var cntp countType

	if val != "" {
		err = json.Unmarshal([]byte(val), &cntp)
		if err != nil {
			log.L(ctx).Errorf("unmarshal error: %v", err)

			return nil, err
		}

		return &cntp, nil
	} else {
		return &countType{
			Count: 0,
			Time:  time.Now(),
		}, nil
	}
}

func setCount(ctx context.Context, key string, count countType) error {
	log.L(ctx).Info("setCount Called.")

	rStore := &rstorage.RedisCluster{
		KeyPrefix: duns,
	}

	duration := time.Now().Sub(count.Time)
	// 超过24小时，重新计数
	if duration.Hours() > 24 {
		count.Count = 0
		count.Time = time.Now()
	}
	count.Count = count.Count + 1
	marshal, _ := json.Marshal(count)

	err := rStore.SetKey(key, string(marshal), time.Hour*24)
	if err != nil {
		return err
	}

	return nil
}

func isBanned(country string) int {
	if country == "CU" || country == "IR" || country == "KP" || country == "SY" || country == "UA-43" || country == "UA" || country == "UA-14" || country == "UA-09" {
		return 1
	}
	return 0
}
