package v1

import (
	"bytes"
	"context"
	"encoding/json"
	"io"
	"net/url"
	"strconv"
	"strings"
	"time"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/jws"
	alertcenter "google.golang.org/api/alertcenter/v1beta1"
	"google.golang.org/api/iamcredentials/v1"

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

	"github.com/pingcap-inc/pchub/pkg/log"
)

type googleAlertService struct {
	store store.Factory
}

type GoogleAlertSrv interface {
	CheckGoogleAlerts(ctx context.Context) error
	GetLastestAlerts(ctx context.Context, hours int64) ([]*alertcenter.Alert, error)
	FilterNewAlerts(ctx context.Context, list []*alertcenter.Alert) ([]*alertcenter.Alert, error)
	HandleNewAlerts(ctx context.Context, list []*alertcenter.Alert) error
}

var _ GoogleAlertSrv = (*googleAlertService)(nil)

func newGoogleAlert(srv *service) *googleAlertService {
	return &googleAlertService{store: srv.store}
}

const (
	dwdGCPSA             = "google-ws-admin-sdk@gcp-digital-infra.iam.gserviceaccount.com"
	dwdGCPSubject        = "liuy@pingcap.com"
	gcpCustomerID        = "022md9sr"
	gcpTokenEP           = "https://oauth2.googleapis.com/token"
	phishingApprovalCode = "44F79E0D-5869-4F33-8070-DED6B1131AA5"
	defaultUserLarkID    = "e359137a"
	defaultUserOpLarkID  = "ou_560c70c17d7173f10747f40539eccc4e"
)

func (p *googleAlertService) CheckGoogleAlerts(ctx context.Context) error {
	as, err := p.GetLastestAlerts(ctx, 24)
	if err != nil {
		log.L(ctx).Errorf("error when call GoogleAlertSrv.GetLastest24HAlerts, err: %s", err.Error())

		return err
	}
	if len(as) == 0 {
		log.L(ctx).Info("no phishing(user reported) alerts in the latest 24 hour")

		return nil
	}

	newAlerts, err := p.FilterNewAlerts(ctx, as)
	if err != nil {
		log.L(ctx).Errorf("error when call GoogleAlertSrv.FilterNewAlerts, err: %s", err.Error())

		return err
	}
	if len(newAlerts) == 0 {
		log.L(ctx).Info("no new phishing(user reported) alerts in the latest 24 hour")

		return nil
	}

	err = p.HandleNewAlerts(ctx, newAlerts)
	if err != nil {
		log.L(ctx).Errorf("error when call GoogleAlertSrv.HandleNewAlerts, err: %s", err.Error())

		return err
	}

	return nil
}

type ApprovalLarkResData struct {
	InstanceCode string `json:"instance_code"`
}

func (p *googleAlertService) HandleNewAlerts(ctx context.Context, list []*alertcenter.Alert) error {
	comSrv := &commonService{store: p.store}
	t, err := comSrv.GetLarkToken(ctx, pchub)
	if err != nil {
		log.L(ctx).Errorf("error when call CommonSrv.GetLarkToken, err: %s", err.Error())

		return err
	}

	newAlerts := make([]*v1.Alert, 0)
	for _, a := range list {
		b, err := p.getApprovalReqBody(ctx, a)
		if err != nil {
			log.L(ctx).Errorf("error when GoogleAlertSrv.getApprovalReqBody, err: %s", err.Error())

			continue
		}

		reqb, err := json.Marshal(&b)
		if err != nil {
			log.L(ctx).Errorf("error when marshal approval request body, err: %s", err.Error())

			continue
		}

		approvalR, err := comSrv.GetDataByReqLarkAPI(ctx, "POST", LarkOpenAPIBase+"/approval/v4/instances", bytes.NewReader(reqb), nil, t)
		if err != nil {
			log.L(ctx).Errorf("error when submit lark approval, err: %s", err.Error())

			continue
		}

		var apprData ApprovalLarkResData
		if err := json.Unmarshal(approvalR, &apprData); err != nil {
			log.L(ctx).Errorf("error when unmarshal approval res data, err: %s", err.Error())

			continue
		}

		na := v1.Alert{
			AlertID:          a.AlertId,
			LarkInstanceCode: apprData.InstanceCode,
		}

		newAlerts = append(newAlerts, &na)
	}

	err = p.store.Alert().MCreate(ctx, newAlerts)
	if err != nil {
		log.L(ctx).Errorf("error when insert alerts & approvals into db, err: %s", err.Error())

		return nil
	}

	return nil
}

type LarkCreateApprovalBody struct {
	ApprovalCode string `json:"approval_code"`
	UserId       string `json:"user_id,omitempty"`
	OpenId       string `json:"open_id"`
	Form         string `json:"form"`
}

type FormItem struct {
	Id      string      `json:"id"`
	Type    string      `json:"type"`
	Value   interface{} `json:"value,omitempty"`
	OpenIDs []string    `json:"open_ids,omitempty"`
}

func (p *googleAlertService) getApprovalReqBody(ctx context.Context, a *alertcenter.Alert) (*LarkCreateApprovalBody, error) {
	res := LarkCreateApprovalBody{
		ApprovalCode: phishingApprovalCode,
	}

	var phishingData alertcenter.MailPhishing
	if err := json.Unmarshal(a.Data, &phishingData); err != nil {
		log.L(ctx).Errorf("error when unmarshal alert.Data into MailPhishing, err: %s", err.Error())

		return nil, err
	}
	reporterEmail := phishingData.Messages[0].Recipient
	messagesLen := len(phishingData.Messages)
	reporterLarkID := defaultUserLarkID
	reporterOpLarkID := defaultUserOpLarkID
	senderMail := phishingData.MaliciousEntity.FromHeader
	if senderMail == "" {
		senderMail = phishingData.MaliciousEntity.Entity.EmailAddress
	}

	u, err := p.store.User().GetByEmail(ctx, reporterEmail)
	if err != nil || u == nil {
		log.L(ctx).Errorf("no such user with email: %s", reporterEmail)
	} else {
		reporterLarkID = u.LarkUserID
		reporterOpLarkID = u.LarkOpenID
	}

	res.UserId = reporterLarkID
	res.OpenId = reporterOpLarkID

	var resForm []FormItem
	limitLen := 140
	mbs := []rune(phishingData.Messages[0].MessageBodySnippet)
	if cap(mbs) < limitLen {
		limitLen = cap(mbs)
	}
	resForm = append(resForm, FormItem{
		Id:    "widget16614938322000001", // 安全类别
		Type:  "radioV2",
		Value: "l7a2jgxo-jqbkukuvsva-9", // 办公安全
	}, FormItem{
		Id:    "widget16614939701650001", // 资产
		Type:  "radioV2",
		Value: "l9zacui1-0zzwnosj2puf-1", // 邮箱
	}, FormItem{
		Id:    "widget16487195633330001", // 安全问题概述
		Type:  "textarea",
		Value: senderMail + " sent " + strconv.Itoa(messagesLen) + " message(s) that was reported as '" + a.Type + "' by users in your domain. Message Snippet: " + strings.Trim(string(mbs[:limitLen]), " ") + "...",
	}, FormItem{
		Id:    "widget16614941455820001", // 改进建议
		Type:  "textarea",
		Value: "Understand the details of phishing/spam emails and block the sender.",
	}, FormItem{
		Id:    "widget16487195406530001", // 紧急程度
		Type:  "radioV2",
		Value: "l1et1ne6-wlhbvc5yoj-0", // 一般
	}, FormItem{
		Id:    "widget16614941687660001", // 整改责任人
		Type:  "contact",
		Value: []string{"e359137a"}, // 冯帅
	}, FormItem{
		Id:    "widget16614942163000001", // 协同人员
		Type:  "contact",
		Value: []string{"dg67ecda"}, // 褚杰
	}, FormItem{
		Id:    "widget16614942960990001", // 整改完成时间
		Type:  "date",
		Value: time.Now().Add(time.Hour * 24 * 15).Format(time.RFC3339),
	})

	resStr, err := json.Marshal(&resForm)
	if err != nil {
		log.L(ctx).Errorf("can not marshal form: %s", err.Error())

		return nil, err
	}

	res.Form = string(resStr)

	return &res, nil
}

func (p *googleAlertService) FilterNewAlerts(ctx context.Context, list []*alertcenter.Alert) ([]*alertcenter.Alert, error) {
	var res []*alertcenter.Alert

	alertIDs := make([]string, 0, len(list))
	for _, a := range list {
		alertIDs = append(alertIDs, a.AlertId)
	}

	alertsInDB, err := p.store.Alert().MGetByAlertIDs(ctx, alertIDs)
	if err != nil {
		log.L(ctx).Errorf("error when call AlertStore.MGetByAlertIDs, err: %s", err.Error())

		return nil, err
	}
	alertsExistMap := make(map[string]bool)
	for _, ad := range alertsInDB {
		alertsExistMap[ad.AlertID] = true
	}

	for _, a := range list {
		cur := a
		_, ok := alertsExistMap[a.AlertId]
		if !ok {
			res = append(res, cur)
		}
	}

	return res, nil
}

func (p *googleAlertService) GetLastestAlerts(ctx context.Context, hours int64) ([]*alertcenter.Alert, error) {
	scope := []string{
		alertcenter.AppsAlertsScope,
	}

	wsInterface, err := NewGoogleService().WsSrv(ctx, "alertcenter", scope...)
	if err != nil {
		log.L(ctx).Errorf("new google service error: %s", err.Error())

		return nil, err
	}

	pageToken := ""
	var res []*alertcenter.Alert
	for {
		createTimeFilter := ""
		if hours > 0 {
			createTimeFilter = ` AND createTime >= "` + time.Now().Add(-time.Duration(hours)*time.Hour).UTC().Format(time.RFC3339) + `"`
		}

		filterStr := `type:"User reported " AND source="Gmail phishing"` + createTimeFilter

		re, err := wsInterface.(*alertcenter.Service).Alerts.
			List().
			Context(ctx).
			PageToken(pageToken).
			CustomerId(gcpCustomerID).
			Filter(filterStr).
			Do()
		if err != nil {
			log.L(ctx).Errorf("error when call gcp alerts-list api, err: %s", err.Error())

			return nil, err
		}

		for _, a := range re.Alerts {
			if a.Type == "User reported phishing" || a.Type == "User reported spam spike" {
				res = append(res, a)
			}
		}

		pageToken = re.NextPageToken

		if pageToken == "" {
			break
		}
	}

	return res, nil
}

type GPayload struct {
	Iss   string `json:"iss"`
	Scope string `json:"scope"`
	Aud   string `json:"aud"`
	Exp   int64  `json:"exp"`
	Iat   int64  `json:"iat"`
	Sub   string `json:"sub"`
}

func getTokenSource(ctx context.Context, scope string) (*oauth2.TokenSource, error) {
	i, err := iamcredentials.NewService(ctx)
	if err != nil {
		log.L(ctx).Errorf("error when iamcredentials.NewService, err: %s", err.Error())

		return nil, err
	}

	p := &GPayload{
		Iss:   dwdGCPSA,
		Scope: scope,
		Aud:   gcpTokenEP,
		Exp:   time.Now().Add(time.Minute * 60).Unix(),
		Iat:   time.Now().Unix(),
		Sub:   dwdGCPSubject,
	}
	py, err := json.Marshal(p)
	if err != nil {
		log.L(ctx).Errorf("error when marshal google jwt payload, err: %s", err.Error())

		return nil, err
	}
	jwtres, err := iamcredentials.NewProjectsServiceAccountsService(i).SignJwt("projects/-/serviceAccounts/"+dwdGCPSA, &iamcredentials.SignJwtRequest{
		Payload: string(py),
	}).Context(ctx).Do()

	if err != nil {
		log.L(ctx).Errorf("error when get jwt token, err: %s", err.Error())

		return nil, err
	}

	h := oauth2.NewClient(ctx, nil)
	v := url.Values{}
	v.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
	v.Set("assertion", jwtres.SignedJwt)
	resp, err := h.PostForm(gcpTokenEP, v)
	if err != nil {
		log.L(ctx).Errorf("error when get access token by jwt, err: %s", err.Error())

		return nil, err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		log.L(ctx).Errorf("error when read gcp token res, err: %s", err.Error())

		return nil, err
	}
	if c := resp.StatusCode; c < 200 || c > 299 {
		log.L(ctx).Errorf("error because the code gcp-token-api returned is not 2xx , err: %s", err.Error())

		return nil, err
	}

	// tokenRes is the JSON response body.
	var tokenRes struct {
		AccessToken string `json:"access_token"`
		TokenType   string `json:"token_type"`
		IDToken     string `json:"id_token"`
		ExpiresIn   int64  `json:"expires_in"` // relative seconds from now
	}
	if err := json.Unmarshal(body, &tokenRes); err != nil {
		log.L(ctx).Errorf("error when unmarshal gcp token res, err: %s", err.Error())

		return nil, err
	}
	token := &oauth2.Token{
		AccessToken: tokenRes.AccessToken,
		TokenType:   tokenRes.TokenType,
	}
	raw := make(map[string]interface{})
	if err := json.Unmarshal(body, &raw); err != nil {
		log.L(ctx).Errorf("error when unmarshal gcp token res into map, err: %s", err.Error())

		return nil, err
	}
	token = token.WithExtra(raw)

	if secs := tokenRes.ExpiresIn; secs > 0 {
		token.Expiry = time.Now().Add(time.Duration(secs) * time.Second)
	}
	if v := tokenRes.IDToken; v != "" {
		// decode returned id token to get expiry
		claimSet, err := jws.Decode(v)
		if err != nil {
			log.L(ctx).Errorf("error when call jws.Decode, err: %s", err.Error())

			return nil, err
		}
		token.Expiry = time.Unix(claimSet.Exp, 0)
	}

	res := oauth2.StaticTokenSource(token)

	return &res, nil
}
