package v1

import (
	"context"
	"encoding/base64"
	"io/ioutil"
	"os"
	"path/filepath"
	"strconv"
	"time"

	"github.com/golang/freetype/truetype"
	lark "github.com/larksuite/oapi-sdk-go/v3"
	larkbitable "github.com/larksuite/oapi-sdk-go/v3/service/bitable/v1"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
	"github.com/spf13/viper"
	"github.com/vicanso/go-charts/v2"
	"github.com/wcharczuk/go-chart"
	"github.com/wcharczuk/go-chart/v2/drawing"
	"google.golang.org/api/gmail/v1"

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

type monthlyReportSrv interface {
	GetMonthlyReport(ctx context.Context) error
}

type monthlyReportService struct {
	store store.Factory
}

var _ monthlyReportSrv = (*monthlyReportService)(nil)

func NewMonthlyReportService(srv *service) monthlyReportSrv {
	return &monthlyReportService{
		store: srv.store,
	}
}

const (
	tableToken = "bascnG92JXGINAjz6ci2ZvVcptd"
	tableId    = "tblALDG1qbCOIb9E"
	viewId     = "vewhHuBknt"
)

func (s *monthlyReportService) GetMonthlyReport(ctx context.Context) error {
	log.L(ctx).Info("GetMonthlyReport Called.")

	records, err := s.getRecords(ctx)
	if err != nil {
		return err
	}
	var monthRecords []*larkbitable.AppTableRecord
	for _, record := range records {
		timestamp := int64(record.Fields["本行信息记录时间"].(float64)) / 1000
		if isLastMontLah(time.Unix(timestamp, 0)) {
			monthRecords = append(monthRecords, record)
		}
	}
	x, y, _ := s.makePoint(ctx, records)
	err = s.monthlyReport(ctx, x, y, 1)
	if err != nil {
		return err
	}
	x, y, _ = s.makePoint(ctx, monthRecords)
	err = s.monthlyReport(ctx, x, y, 2)
	if err != nil {
		return err
	}
	err = s.SendMail(ctx, x, y, len(monthRecords))
	if err != nil {
		log.L(ctx).Errorf("send mail failed: %v", err)

		return err
	}

	log.L(ctx).Info("GetMonthlyReport Done.")

	return nil
}

func (s *monthlyReportService) monthlyReport(ctx context.Context, x [][]string, y [][]float64, tp int) error {

	legend := []string{"计数"}
	title := "安全运营状态分布"
	err := s.MakeCharts(ctx, x[2], [][]float64{y[2]}, nil, title, legend, 0, tp)
	if err != nil {
		return err
	}
	title = "安全类别"
	err = s.MakeCharts(ctx, x[0], [][]float64{y[0]}, nil, title, legend, 0, tp)
	if err != nil {
		return err
	}
	title = "资产分布"
	err = s.MakeCharts(ctx, x[1], [][]float64{y[1]}, nil, title, legend, 0, tp)
	if err != nil {
		return err
	}
	title = "安全运营事件分类"
	err = s.MakeCharts(ctx, x[0], [][]float64{y[0]}, nil, title, x[0], 1, tp)
	if err != nil {
		return err
	}
	title = "安全运营资产分类"
	err = s.MakeCharts(ctx, x[1], [][]float64{y[1]}, nil, title, x[1], 1, tp)
	if err != nil {
		return err
	}

	return nil
}

func (s *monthlyReportService) makePoint(_ context.Context, records []*larkbitable.AppTableRecord) ([][]string, [][]float64, []map[string]int) {
	var x [][]string
	var y [][]float64

	xApproval := make(map[string]int)
	xType := make(map[string]int)
	xAsset := make(map[string]int)
	xDate := make(map[string]map[string]int)
	for _, record := range records {
		apprStatus := record.Fields["审批状态"]
		safeType := record.Fields["安全类别*"]
		asset := record.Fields["资产*"]
		rdTime := record.Fields["本行信息记录时间"]
		if apprStatus == nil || safeType == nil || asset == nil || rdTime == nil {
			continue
		}
		xApproval[apprStatus.(string)]++
		t := safeType.(string)
		xType[t]++
		xAsset[asset.(string)]++
		timestamp := int64(rdTime.(float64)) / 1000
		d := time.Unix(timestamp, 0).Format("2006/01/02")
		m := xDate[d]
		if m == nil {
			m = make(map[string]int)
		}
		m[t]++
		xDate[d] = m
	}
	var xt []string
	var yt []float64
	for k, v := range xType {
		xt = append(xt, k)
		yt = append(yt, float64(v))
	}
	x = append(x, xt)
	y = append(y, yt)
	xt = nil
	yt = nil
	for k, v := range xAsset {
		xt = append(xt, k)
		yt = append(yt, float64(v))
	}
	x = append(x, xt)
	y = append(y, yt)
	xt = nil
	yt = nil
	for k, v := range xApproval {
		xt = append(xt, k)
		yt = append(yt, float64(v))
	}
	x = append(x, xt)
	y = append(y, yt)
	xt = nil
	var yd []map[string]int
	for k, v := range xDate {
		xt = append(xt, k)
		yd = append(yd, v)
	}
	x = append(x, xt)

	return x, y, yd
}

func (s *monthlyReportService) getRecords(ctx context.Context) ([]*larkbitable.AppTableRecord, error) {
	id := viper.GetString("lark.appId")
	secret := viper.GetString("lark.appSecret")
	client := lark.NewClient(id, secret)
	var pageToken string
	var list []*larkbitable.AppTableRecord
	for {
		req := larkbitable.
			NewListAppTableRecordReqBuilder().AppToken(tableToken).
			TableId(tableId).
			ViewId(viewId).
			PageSize(500)
		if pageToken != "" {
			req.PageToken(pageToken)
		}
		res, err := client.Bitable.AppTableRecord.List(ctx, req.Build())
		if err != nil {
			log.L(ctx).Errorf("get monthly report error: %s", err.Error())

			return nil, err
		}
		if res.CodeError.Code != 0 {
			log.L(ctx).Errorf("get monthly report error: %s", res.Msg)

			return nil, errors.New(res.Msg)
		}
		list = append(list, res.Data.Items...)

		resData := *res.Data
		if resData.PageToken == nil {
			break
		} else {
			pageToken = *resData.PageToken
		}
	}

	return list, nil
}

func (s *monthlyReportService) MakeCharts(ctx context.Context, x []string, y [][]float64, yd []map[string]int, title string, legend []string, chartType, tp int) error {
	buff, err := ioutil.ReadFile("./data/chinese.ttf")
	if err != nil {
		log.L(ctx).Errorf("read font error: %s", err.Error())

		return err
	}
	err = charts.InstallFont("noto", buff)
	if err != nil {
		log.L(ctx).Errorf("install font error: %s", err.Error())

		return err
	}

	var p *charts.Painter
	show := true
	switch chartType {
	case 0:
		p, err = charts.BarRender(
			y,
			charts.XAxisOptionFunc(charts.XAxisOption{
				Show:        &show,
				StrokeColor: drawing.ColorBlue,
				FontColor:   drawing.ColorBlue,
			}),
			charts.YAxisDataOptionFunc(x),
			charts.FontFamilyOptionFunc("noto"),
			charts.TitleOptionFunc(charts.TitleOption{
				Text:     title,
				FontSize: 18,
				Left:     charts.PositionCenter,
			}),
			charts.XAxisDataOptionFunc(x),
			charts.PaddingOptionFunc(charts.Box{
				Top:    20,
				Right:  40,
				Bottom: 20,
				Left:   20,
			}),
			charts.LegendOptionFunc(charts.LegendOption{
				Orient: charts.OrientVertical,
				Data:   legend,
				Left:   charts.PositionRight,
			}),
			charts.HeightOptionFunc(600),
			charts.WidthOptionFunc(800),
		)
		if err != nil {
			log.L(ctx).Errorf("charts bar render error: %s", err.Error())

			return err
		}
	case 1:
		p, err = charts.PieRender(
			y[0],
			charts.FontFamilyOptionFunc("noto"),
			charts.TitleOptionFunc(charts.TitleOption{
				Text:     title,
				FontSize: 18,
				Left:     charts.PositionCenter,
			}),

			charts.LegendOptionFunc(charts.LegendOption{
				Orient: charts.OrientVertical,
				Data:   x,
				Left:   charts.PositionLeft,
			}),
			charts.PieSeriesShowLabel(),
			charts.HeightOptionFunc(600),
			charts.WidthOptionFunc(800),
		)
		p.SetTextStyle(charts.Style{
			FontSize: 3,
		})
		if err != nil {
			log.L(ctx).Errorf("charts pie render error: %s", err.Error())

			return err
		}
	case 2:
		barwidth := 10
		var bars []chart.StackedBar
		for i := 0; i < len(x); i++ {
			var charValues []chart.Value
			for _, le := range legend {
				f := float64(yd[i][le])
				if f == 0 {
					continue
				}
				chartValue := chart.Value{
					Label: le,
					Value: float64(yd[i][le]),
					Style: chart.Style{
						StrokeWidth: .01,
					},
				}
				charValues = append(charValues, chartValue)
			}
			bart := chart.StackedBar{
				Name:   x[i],
				Values: charValues,
				Width:  barwidth,
			}
			bars = append(bars, bart)
		}
		stackedBarChart := chart.StackedBarChart{
			Title: title,
			TitleStyle: chart.Style{
				Show: true,
			},
			Background: chart.Style{
				FillColor: chart.ColorWhite,
			},
			YAxis: chart.Style{
				Show:        true,
				StrokeWidth: 0.1,
			},
			Bars: bars,
		}

		pngFile, err := os.Create("./data/" + title + ".png")
		if err != nil {
			log.L(ctx).Errorf("create file error: %s", err.Error())

			return err
		}
		font, err := truetype.Parse(buff)
		if err != nil {
			log.L(ctx).Errorf("parse font error: %s", err.Error())

			return err
		}
		stackedBarChart.Font = font
		if err = stackedBarChart.Render(chart.PNG, pngFile); err != nil {
			log.L(ctx).Errorf("render chart error: %s", err.Error())

			return err
		}

		if err = pngFile.Close(); err != nil {
			log.L(ctx).Errorf("close file error: %s", err.Error())

			return err
		}

		return nil
	default:
		log.L(ctx).Errorf("chart type error: %d", chartType)

		return errors.New("chart type error")
	}
	buf, err := p.Bytes()
	if err != nil {
		panic(err)
	}
	err = writeFile(ctx, title, buf, tp)
	if err != nil {
		log.L(ctx).Errorf("write file error: %s", err.Error())

		return err
	}

	return nil
}

func (s *monthlyReportService) SendMail(ctx context.Context, x [][]string, y [][]float64, total int) error {
	log.L(ctx).Info("SendMail Called.")

	f1, f2 := getTopTwo(x[0], y[0])
	msg := "<h3>本月累计产生安全运营流程<b style='font-weight:bold;color:red;font-size: 20px;'>" + strconv.Itoa(total) + "</b>条，其中主要工作重心的方向在<b style='font-weight:bold;color:red;font-size: 20px;'>" + f1 + "</b>和<b style='font-weight:bold;color:red;font-size: 20px;'>" + f2 + "</b>，"
	f1, f2 = getTopTwo(x[1], y[1])
	msg += "安全重点治理的资产是<b style='font-weight:bold;color:red;font-size: 20px;'>" + f1 + "</b>和<b style='font-weight:bold;color:red;font-size: 20px;'>" + f2 + "</b></h3>"
	to := "xiaopeng.zhu@pingcap.com,shuai.feng@pingcap.com,ken.liu@pingcap.com"
	subject := "Security Ops Monthly Report"
	body := `
		<html>
		<body>
			<h1>1、当前总量：</h1>
			<div>
			<img src="cid:total1.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:total2.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:total3.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:total4.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:total0.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<div>
            <h1>2、月度数据：</h1>
			<div>
			<img src="cid:month1.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:month2.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:month3.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:month4.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<img src="cid:month0.png" style="display:inline-block;margin-right:10px;width:180px;height:200px;">
			<div>
			<p>` + msg + `</p>
		</body>
		</html>
	`
	var images string
	listt, err := getFileList(ctx, "./data/total")
	if err != nil {
		log.L(ctx).Errorf("get file list error: %s", err.Error())

		return err
	}
	for i, v := range listt {
		imageData, err := ioutil.ReadFile(v)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		// 将图片数据编码为 base64 格式
		imageBase64 := base64.StdEncoding.EncodeToString(imageData)

		str := "--myboundary\r\n" +
			"Content-Type: image/png\r\n" +
			"Content-Disposition: inline; filename=\"total" + strconv.Itoa(i) + ".png\"\r\n" +
			"Content-ID: total" + strconv.Itoa(i) + ".png\r\n" +
			"Content-Transfer-Encoding: base64\r\n" +
			"\r\n" + imageBase64 + "\r\n"
		images += str
	}
	listm, err := getFileList(ctx, "./data/month")
	if err != nil {
		log.L(ctx).Errorf("get file list error: %s", err.Error())

		return err
	}
	for i, v := range listm {
		imageData, err := ioutil.ReadFile(v)
		if err != nil {
			log.Fatalf("Error: %v", err)
		}

		// 将图片数据编码为 base64 格式
		imageBase64 := base64.StdEncoding.EncodeToString(imageData)

		str :=
			"--myboundary\r\n" +
				"Content-Type: image/png\r\n" +
				"Content-Disposition: inline; filename=\"month" + strconv.Itoa(i) + ".png\"\r\n" +
				"Content-ID: month" + strconv.Itoa(i) + ".png\r\n" +
				"Content-Transfer-Encoding: base64\r\n" +
				"\r\n" + imageBase64 + "\r\n"
		images += str
	}
	images += "--myboundary--"
	mes := []byte(
		"To: " + to + "\r\n" +
			"Subject: " + subject + "\r\n" +
			"MIME-Version: 1.0\r\n" +
			"Content-Type: multipart/related; boundary=myboundary\r\n\r\n" +
			"--myboundary\r\n" +
			"Content-Type: text/html\r\n" +
			"\r\n" + body + "\r\n\r\n" +
			images)
	// 创建邮件消息
	message := &gmail.Message{
		Raw: base64.RawURLEncoding.EncodeToString(mes),
	}

	scope := []string{
		gmail.GmailSendScope,
	}

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

		return err
	}

	_, err = wsInterface.(*gmail.Service).Users.Messages.Send("me", message).Do()
	if err != nil {
		log.L(ctx).Errorf("error when send email, err: %s", err.Error())

		return err
	}

	return nil
}

func getTopTwo(x []string, y []float64) (string, string) {
	if len(y) == 1 {
		return x[0], "无"
	}
	if len(y) == 0 {
		return "无", "无"
	}

	// 找出 y 数组中的最大值和次大值的索引
	maxIndex, secondMaxIndex := 0, 1
	if y[1] > y[0] {
		maxIndex, secondMaxIndex = 1, 0
	}
	for i := 2; i < len(y); i++ {
		if y[i] > y[maxIndex] {
			secondMaxIndex = maxIndex
			maxIndex = i
		} else if y[i] > y[secondMaxIndex] {
			secondMaxIndex = i
		}
	}

	// 返回最大值和次大值对应的 x 值
	return x[maxIndex], x[secondMaxIndex]
}

func getFileList(ctx context.Context, root string) ([]string, error) {
	var fileList []string
	// 定义一个回调函数，用于处理每个文件
	callback := func(path string, info os.FileInfo, err error) error {
		if err != nil {
			// 处理错误
			return err
		}
		if !info.IsDir() {
			// 打印文件路径
			fileList = append(fileList, path)
		}
		return nil
	}
	// 调用 walkDir 函数，打印每个文件的路径
	err := filepath.Walk(root, callback)
	if err != nil {
		log.L(ctx).Errorf("get file list error: %s", err.Error())

		return nil, err
	}

	return fileList, nil
}

func writeFile(ctx context.Context, title string, buf []byte, tp int) error {
	var tmpPath string
	if tp == 1 {
		tmpPath = "./data/total"
	} else {
		tmpPath = "./data/month"
	}
	err := os.MkdirAll(tmpPath, 0700)
	if err != nil {
		log.L(ctx).Errorf("make dir error: %s", err.Error())

		return err
	}

	file := filepath.Join(tmpPath, title+".png")
	err = ioutil.WriteFile(file, buf, 0600)
	if err != nil {
		log.L(ctx).Errorf("write file error: %s", err.Error())

		return err
	}
	return nil
}

func isLastMontLah(date time.Time) bool {
	now := time.Now()
	lastMonth := now.AddDate(0, -1, 0)

	// 判断是否上个月
	if date.Year() == lastMonth.Year() && date.Month() == lastMonth.Month() {
		return true
	}

	return false
}
