package v1

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

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

	"github.com/pingcap-inc/security/pkg/rstorage"

	"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-"
)

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

	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
	}

	for _, rd := range records {
		if rd.Status != 0 {
			continue
		}
		rd.Status = 1

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

			continue
		}

		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++
					}
				}
			}
		}

		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)
		}
	}

	rds := make([]*v1.Company, 0)
	errTag := false
	for _, rd := range records {
		if rd.Status == 1 {
			rds = append(rds, rd)
		}
		if rd.Status == 2 {
			errTag = true
		}
	}

	if len(rds) != 0 {

		comSrv := newCommon()
		// use old lark
		tok, err := comSrv.GetLarkToken(ctx, 2)
		if err != nil {
			return err
		}

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

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

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

	if errTag {
		return errors.New("dun check error")
	}

	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",
	}
	resp, err := DoDunsRequest(ctx, "GET", url, nil, params, token)
	if err != nil {
		log.L(ctx).Errorf("post error: %v", err)
		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",
	}

	resp, err := DoDunsRequest(ctx, "GET", url, nil, params, token)
	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",
	}

	resp, err := DoDunsRequest(ctx, "GET", url, nil, params, token)
	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,
		}

		basicInfos = append(basicInfos, basicInfo)
	}

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

	return basicInfos, 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 == "" {

		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)
		resp, err := DoDunsRequest(ctx, "POST", url, bytes.NewReader(marshal), nil, base64key+key)
		if err != nil {
			log.L(ctx).Errorf("post error: %v", err)
			return "", 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())
		}
	}

	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: primaryNotificationLarkUserID,
		})
		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 == "RU" || country == "RUS" || country == "CU" || country == "IR" || country == "KP" || country == "SY" || country == "UA-43" || country == "UA" || country == "UA-14" || country == "UA-09" {
		return 1
	}
	return 0
}
