package v1

import (
	"bytes"
	"compress/gzip"
	"context"
	"encoding/json"
	"io/ioutil"
	"strconv"
	"strings"
	"sync"
	"unicode/utf8"

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

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

type docRetrieveService struct {
	store store.Factory
}

type DocRetrieveSrv interface {
	RetrieveDocs(ctx context.Context) error
	GetDocs(ctx context.Context, spaceId, parentNodeToken string, updateOld bool) error
	GetSpaces(ctx context.Context) error
}

var _ DocRetrieveSrv = (*docRetrieveService)(nil)

func newDocRetrieve(srv *service) *docRetrieveService {
	return &docRetrieveService{store: srv.store}
}

const (
	WikiBase      = "https://pingcap.feishu.cn/wiki/"
	AutoCheck     = 1
	UserCheck     = 2
	Unchecked     = 0
	TargetSpaceID = "6989845676184092676" // remove later
	Letters       = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
)

func (dcs *docRetrieveService) RetrieveDocs(ctx context.Context) error {

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

		return err
	}

	err = dcs.GetSpaces(ctx)
	if err != nil {
		return err
	}

	wikiSpaces, err := dcs.store.Wiki().GetWikiSpace(ctx)
	if err != nil {
		log.L(ctx).Errorf("get wiki space error: %v", err)

		return err
	}

	for _, wikiSpace := range wikiSpaces {
		if wikiSpace.SpaceID == TargetSpaceID {
			err = dcs.GetDocs(ctx, wikiSpace.SpaceID, "", false)
			if err != nil {
				return err
			}
		}
	}

	return nil
}

type spaceItem struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	SpaceID     string `json:"space_id"`
}

type respData struct {
	Items     json.RawMessage `json:"items"`
	PageToken string          `json:"page_token"`
	Total     int             `json:"total"`
	HasMore   bool            `json:"has_more"`
}

func (dcs *docRetrieveService) GetSpaces(ctx context.Context) error {
	url := "/wiki/v2/spaces"
	pageToken := ""
	spaces := make([]spaceItem, 0)
	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)
	for {
		params := map[string]string{
			"page_size":  "50",
			"page_token": pageToken,
		}
		jsonResp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, params, tk)

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

			return err
		}

		var respItems []spaceItem
		err = json.Unmarshal(resp.Items, &respItems)
		if err != nil {
			log.L(ctx).Errorf("unmarshal data error: %v", err)

			return err
		}

		spaces = append(spaces, respItems...)
		if !resp.HasMore {
			break
		}
		pageToken = resp.PageToken
	}

	dbWikiSpaces, err := dcs.store.Wiki().GetWikiSpace(ctx)
	if err != nil {
		log.L(ctx).Errorf("get wiki space error: %v", err)

		return err
	}

	dbSpacesMap := make(map[string]int)
	for _, dbWikiSpace := range dbWikiSpaces {
		dbSpacesMap[dbWikiSpace.SpaceID] = 1
	}

	updateWikiSpaces := make([]v1.WikiSpace, 0)
	delWikiSpaces := make([]v1.WikiSpace, 0)
	for _, space := range spaces {
		if _, ok := dbSpacesMap[space.SpaceID]; !ok {
			updateWikiSpaces = append(updateWikiSpaces, v1.WikiSpace{
				SpaceID:     space.SpaceID,
				Name:        space.Name,
				Description: space.Description,
			})
		} else {
			dbSpacesMap[space.SpaceID] = 0
		}
	}

	for spaceID, v := range dbSpacesMap {
		if v == 1 {
			delWikiSpaces = append(delWikiSpaces, v1.WikiSpace{
				SpaceID: spaceID,
			})
		}
	}

	err = dcs.store.Wiki().SaveDelWikiSpaces(ctx, updateWikiSpaces, delWikiSpaces)
	if err != nil {
		log.L(ctx).Errorf("save wiki space error: %v", err)
	}

	return err
}

type nodeItem struct {
	SpaceID         string `json:"space_id"`
	NodeToken       string `json:"node_token"`
	ObjToken        string `json:"obj_token"`
	ObjType         string `json:"obj_type"`
	ParentNodeToken string `json:"parent_node_token"`
	NodeType        string `json:"node_type"`
	OriginNodeToken string `json:"origin_node_token"`
	OriginSpaceID   string `json:"origin_space_id"`
	HasChild        bool   `json:"has_child"`
	Title           string `json:"title"`
	ObjCreateTime   string `json:"obj_create_time"`
	ObjEditTime     string `json:"obj_edit_time"`
	NodeCreateTime  string `json:"node_create_time"`
	Creator         string `json:"creator"`
	Owner           string `json:"owner"`
}

func (dcs *docRetrieveService) GetDocs(ctx context.Context, spaceId, parentNodeToken string, updateOld bool) error {
	var docs []nodeItem
	errCh := make(chan error, 1)
	wg := &sync.WaitGroup{}
	mutex := &sync.Mutex{}

	wg.Add(1)
	go getSpaceNodes(ctx, spaceId, parentNodeToken, &docs, errCh, wg, mutex)

	wg.Wait()
	close(errCh)

	// Check for errors collected via the channel.
	select {
	case err := <-errCh: // 如果有错误，就返回它
		if err != nil {
			return err
		}
	default: // 没有错误
	}

	dbDocs, err := dcs.store.Wiki().GetWikiDocs(ctx)
	if err != nil {
		log.L(ctx).Errorf("get wiki docs error: %v", err)

		return err
	}

	dbDocsMap := make(map[string]int)
	dbDocsObjMap := make(map[string]v1.WikiDoc)
	for _, dbDoc := range dbDocs {
		dbDocsMap[dbDoc.NodeToken] = 1
		dbDocsObjMap[dbDoc.NodeToken] = dbDoc
	}

	updateWikiDocs := make([]v1.WikiDoc, 0)
	delWikiDocs := make([]v1.WikiDoc, 0)
	wg = &sync.WaitGroup{}
	wgMutex := &sync.Mutex{}
	wgChan := make(chan struct{}, 10)
	for _, doc := range docs {
		if _, ok := dbDocsMap[doc.NodeToken]; !ok {
			wg.Add(1)
			wgChan <- struct{}{}
			if doc.ObjType == "docx" {
				wgMutex.Lock()
			}
			go func(doc nodeItem) {
				defer func() {
					wg.Done()
					<-wgChan
					if doc.ObjType == "docx" {
						wgMutex.Unlock()
					}
				}()
				newWikiDoc := v1.WikiDoc{
					SpaceID:   doc.SpaceID,
					ParentID:  doc.ParentNodeToken,
					URL:       WikiBase + doc.NodeToken,
					Title:     doc.Title,
					ObjToken:  doc.ObjToken,
					NodeToken: doc.NodeToken,
					Owner:     doc.Owner,
					Type:      doc.ObjType,
					Level:     0,
					Reason:    Unchecked,
				}

				err := dcs.getAndSaveContent(ctx, newWikiDoc)
				if err != nil {
					log.L(ctx).Errorf("get and save content error: %v", err)
				}
			}(doc)
		} else {
			dbDocsMap[doc.NodeToken] = 0
		}
	}

	wg.Wait()

	// 检查已经保存的文档内容
	if updateOld {
		for nodeToken, v := range dbDocsMap {
			if v == 0 {
				obj := dbDocsObjMap[nodeToken]
				wg.Add(1)
				wgChan <- struct{}{}
				if obj.Type == "docx" {
					wgMutex.Lock()
				}
				go func(nodeToken string) {
					defer func() {
						wg.Done()
						<-wgChan
						if obj.Type == "docx" {
							wgMutex.Unlock()
						}
					}()

					err = dcs.getAndSaveContent(ctx, obj)
					if err != nil {
						log.L(ctx).Errorf("get and save content error: %v", err)
					}
				}(nodeToken)
			}
		}

		wg.Wait()
	}

	for nodeToken, v := range dbDocsMap {
		if v == 1 {
			delWikiDocs = append(delWikiDocs, v1.WikiDoc{
				NodeToken: nodeToken,
			})
		}
	}

	err = dcs.store.Wiki().SaveDelWikiDocs(ctx, updateWikiDocs, delWikiDocs)
	if err != nil {
		log.L(ctx).Errorf("save wiki docs error: %v", err)
	}

	return err
}

func getSpaceNodes(ctx context.Context, spaceId, parentNodeToken string, docs *[]nodeItem, errCh chan error, wg *sync.WaitGroup, mutex *sync.Mutex) {
	defer wg.Done()

	url := "/wiki/v2/spaces/" + spaceId + "/nodes"
	pageToken := ""

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)
	for {
		params := map[string]string{
			"page_size":         "50",
			"page_token":        pageToken,
			"parent_node_token": parentNodeToken,
		}
		jsonResp, err := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, params, tk)
		if err != nil {
			errCh <- err // 发送错误
			return
		}

		var resp respData
		err = json.Unmarshal(jsonResp, &resp)
		if err != nil {
			errCh <- err // 发送错误
			return
		}

		var respItems []nodeItem
		err = json.Unmarshal(resp.Items, &respItems)
		if err != nil {
			errCh <- err // 发送错误
			return
		}

		mutex.Lock()
		for _, item := range respItems {
			*docs = append(*docs, item) // 现在每个节点都是独立添加
			if item.HasChild {
				wg.Add(1)
				go getSpaceNodes(ctx, spaceId, item.NodeToken, docs, errCh, wg, mutex)
			}
		}
		mutex.Unlock()

		if !resp.HasMore {
			break
		}
		pageToken = resp.PageToken
	}
}

func (dcs *docRetrieveService) getAndSaveContent(ctx context.Context, wikiDoc v1.WikiDoc) error {
	log.L(ctx).Infof("start to get content for doc: %s", wikiDoc.NodeToken)

	content, err := getDocContent(ctx, wikiDoc.ObjToken, wikiDoc.Type)
	if err != nil {
		return err
	}

	content = removeNonUTF8MB3Characters(content)
	wikiDoc.Content, err = Compress(content)

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

		return err
	}

	err = dcs.store.Wiki().SaveWikiDoc(ctx, wikiDoc)
	if err != nil {
		log.L(ctx).Errorf("save wiki doc error: %v", err)

		return err
	}

	return nil
}

type docRespData struct {
	Content string `json:"content"`
}

func getDocContent(ctx context.Context, docToken, docType string) (string, error) {

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	var content string
	var err error
	switch docType {
	case "doc", "docx":
		url := "/doc/v2/" + docToken + "/raw_content"
		if docType == "docx" {
			url = "/docx/v1/documents/" + docToken + "/raw_content"
		}
		jsonResp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, nil, tk)

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

			return "", err
		}

		content = resp.Content
	case "bitable":
		content, err = getBiTable(ctx, docToken)
		if err != nil {
			return "", err
		}
	case "sheet":
		content, err = getSheet(ctx, docToken)
	case "mindnote":
	}

	return content, nil
}

type biTableItem struct {
	TableId  string `json:"table_id"`
	Revision int    `json:"revision"`
	Name     string `json:"name"`
}

func getBiTable(ctx context.Context, docToken string) (string, error) {

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	tableUrl := "/bitable/v1/apps/" + docToken + "/tables"
	jsonResp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+tableUrl, nil, nil, tk)
	var content string
	var respData respData
	err := json.Unmarshal(jsonResp, &respData)
	if err != nil {
		log.L(ctx).Errorf("unmarshal data error: %v", err)

		return "", err
	}
	var respItems []biTableItem
	err = json.Unmarshal(respData.Items, &respItems)
	if err != nil {
		log.L(ctx).Errorf("unmarshal data error: %v", err)

		return "", err
	}

	for _, item := range respItems {
		tmpContent, err := getTableRecords(ctx, docToken, item.TableId)
		if err != nil {
			log.L(ctx).Errorf("get table records error: %v", err)

			return "", err
		}

		content += tmpContent
	}

	return content, nil
}

func getTableRecords(ctx context.Context, docToken, tableId string) (string, error) {
	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	url := "/bitable/v1/apps/" + docToken + "/tables/" + tableId + "/records"
	var pageToken, content string

	for {
		params := map[string]string{
			"page_size":  "500",
			"page_token": pageToken,
		}
		jsonResp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, params, tk)

		var respData respData
		err := json.Unmarshal(jsonResp, &respData)
		if err != nil {
			log.L(ctx).Errorf("unmarshal data error: %v", err)

			return "", err
		}

		content += string(respData.Items)

		if respData.HasMore {
			pageToken = respData.PageToken
		} else {
			break
		}
	}

	return content, nil
}

type Spreadsheet struct {
	Sheets []Sheets `json:"sheets"`
}

type Sheets struct {
	SheetID   string     `json:"sheetId"`
	Title     string     `json:"title"`
	BlockInfo *BlockInfo `json:"blockInfo"` // optional field
}

type BlockInfo struct {
	BlockToken string `json:"blockToken"`
	BlockType  string `json:"blockType"`
}

func getSheet(ctx context.Context, docToken string) (string, error) {

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	url := "/sheets/v2/spreadsheets/" + docToken + "/metainfo"

	jsonResp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, nil, tk)

	var respData Spreadsheet
	err := json.Unmarshal(jsonResp, &respData)
	if err != nil {
		log.L(ctx).Errorf("unmarshal data error: %v", err)

		return "", err
	}

	content := ""
	for _, sheet := range respData.Sheets {
		tmpContent := ""
		if sheet.BlockInfo != nil && sheet.BlockInfo.BlockType == "BITABLE_BLOCK" {
			split := strings.Split(sheet.BlockInfo.BlockToken, "_")
			tmpContent, err = getBiTable(ctx, split[0])
			if err != nil {
				return "", err
			}
		} else {
			tmpContent, err = getSheetData(ctx, docToken, sheet.SheetID)
			if err != nil {
				return "", err
			}
		}

		content += tmpContent
	}

	return content, nil
}

type sheetDataResp struct {
	Revision         int    `json:"revision"`
	SpreadsheetToken string `json:"spreadsheetToken"`
	ValueRange       struct {
		MajorDimension string          `json:"majorDimension"`
		Range          string          `json:"range"`
		Revision       int             `json:"revision"`
		Values         [][]interface{} `json:"values"`
	} `json:"valueRange"`
}

func getSheetData(ctx context.Context, docToken string, sheetID string) (string, error) {

	comSrv := newCommon()
	tk, _ := comSrv.GetLarkToken(ctx, 1)

	rangeFormat := sheetID + "!A1:Z300"
	url := "/sheets/v2/spreadsheets/" + docToken + "/values/" + rangeFormat

	params := map[string]string{
		"valueRenderOption": "ToString",
	}

	jsonRsp, _ := comSrv.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+url, nil, params, tk)

	var respData sheetDataResp
	err := json.Unmarshal(jsonRsp, &respData)
	if err != nil {
		log.L(ctx).Errorf("unmarshal data error: %v", err)

		return "", err
	}

	validContent := ""
	for _, value := range respData.ValueRange.Values {
		for _, v := range value {
			if v == nil {
				break
			}

			s, ok := v.(string)
			if ok {
				validContent = validContent + " " + s

				continue
			}

			f, ok := v.(int)
			if ok {
				validContent = validContent + " " + strconv.Itoa(f)
			}

			// others drop
		}
	}

	return validContent, nil
}

func removeNonUTF8MB3Characters(input string) string {
	input = strings.ReplaceAll(input, " ", "")
	input = strings.ReplaceAll(input, "\n", "")

	output := make([]rune, 0, len(input))

	for _, r := range input {
		if r <= utf8.MaxRune && utf8.RuneLen(r) <= 3 {
			output = append(output, r)
		}
	}

	return string(output)
}

// 压缩函数
func Compress(data string) ([]byte, error) {
	var b bytes.Buffer
	gz := gzip.NewWriter(&b)
	if _, err := gz.Write([]byte(data)); err != nil {
		return nil, err
	}
	if err := gz.Flush(); err != nil {
		return nil, err
	}
	if err := gz.Close(); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

// 解压缩函数
func Decompress(data []byte) (string, error) {
	gz, err := gzip.NewReader(bytes.NewBuffer(data))
	if err != nil {
		return "", err
	}
	defer gz.Close()
	res, err := ioutil.ReadAll(gz)
	if err != nil {
		return "", err
	}
	return string(res), nil
}
