package v1

import (
	"encoding/json"
	"github.com/pingcap-inc/pinsights/internal/pkg/authz"
	"io/ioutil"
	"net/http"
	"strconv"
	"strings"
	"time"

	"github.com/marmotedu/errors"
	"github.com/pingcap-inc/pinsights/internal/larkbot/store"
	"github.com/pingcap-inc/pinsights/internal/pkg/code"
	v1 "github.com/pingcap-inc/pinsights/model/v1"
	"github.com/pingcap-inc/pinsights/pkg/log"
	"github.com/pingcap-inc/pinsights/pkg/util/jsonc"
)

type ContactSrv interface {
	Refresh() error
}

type contactSrv struct {
	store store.Factory
}

var _ ContactSrv = (*contactSrv)(nil)

func newContactSrv(srv *service) *contactSrv {
	return &contactSrv{store: srv.store}
}

type LarkDptsData struct {
	Items     []LarkDptItem `json:"items"`
	PageToken string        `json:"pageToken"`
	HasMore   bool          `json:"hasMore"`
}

type LarkDptStatus struct {
	IsDeleted bool `json:"isDeleted"`
}

type LarkDptItem struct {
	DepartmentID       string        `json:"departmentId"`
	Name               string        `json:"name"`
	ParentDepartmentID string        `json:"parentDepartmentId"`
	Status             LarkDptStatus `json:"status"`
}

func (c *contactSrv) Refresh() error {
	log.Info("Try to refresh contact from lark.")

	larkCommonService := newLarkCommon()
	tat, err := larkCommonService.GetAndCacheP7STToken()
	if err != nil {
		log.Error(err.Error())
		return errors.Wrap(err, "Error when refresh contact bacause of the error in GetAndCacheP7STToken.")
	}

	larkDpts, err := getLarkDpts(tat, "")
	if err != nil {
		log.Error(err.Error())
		return errors.Wrap(err, "Error when get lark departments bacause of the error in getLarkDpts.")
	}

	log.Info("Success to get lark departments, total: " + strconv.Itoa(len(*larkDpts)))

	gs, err := c.store.Groups().ListLarkDpts()
	if err != nil {
		log.Error(err.Error())
		return errors.Wrap(err, "Error when list groups from db.")
	}
	nGS := make(map[string]*v1.Group)
	for _, g := range gs.Items {
		nGS[g.SourceID] = g
	}

	// get casbin enforcer
	e, err := authz.GetCasbinEnforcer(nil)
	if err != nil {
		log.Error(err.Error())
		return errors.Wrap(err, "Error when GetCasbinEnforcer.")
	}

	allLarkCasbinGroups := e.GetNamedGroupingPolicy("g")
	nCGS := make(map[string][]string)
	var larkCasbinGroups [][]string
	for _, cg := range allLarkCasbinGroups {
		if strings.HasPrefix(cg[0], "g_") {
			larkCasbinGroups = append(larkCasbinGroups, cg)
			nCGS[cg[0]] = cg
		}
	}

	// diff groups
	var newGroupList []v1.Group
	var updateGroupList []*v1.Group
	var delIdsGroupList []uint64
	var larkDptIds = make(map[string]bool, len(*larkDpts))

	var updateOldCasbinGroupList [][]string
	var updateNewCasbinGroupList [][]string
	var addNewCasbinGroupList [][]string
	var delCasbinGroupList [][]string
	for _, dpt := range *larkDpts {
		if dpt.Status.IsDeleted {
			continue
		}
		gInDB, ok := nGS[dpt.DepartmentID]
		casbinG, hasCasbinRule := nCGS["g_"+dpt.DepartmentID]
		larkDptIds["g_"+dpt.DepartmentID] = true
		if ok {
			parentChange := dpt.ParentDepartmentID != gInDB.ParentSourceID
			if dpt.Name != gInDB.Name || parentChange {
				updateG := gInDB
				updateG.Name = dpt.Name
				updateG.ParentSourceID = dpt.ParentDepartmentID
				updateGroupList = append(updateGroupList, updateG)
			}
		} else {
			newG := v1.Group{
				Type:           1,
				SourceID:       dpt.DepartmentID,
				ParentSourceID: dpt.ParentDepartmentID,
				Name:           dpt.Name,
				CreatedAt:      time.Now(),
			}
			newGroupList = append(newGroupList, newG)
		}

		if hasCasbinRule && casbinG[1] != "g_"+dpt.ParentDepartmentID {
			updateOldCasbinGroupList = append(updateOldCasbinGroupList, casbinG)
			updateNewCasbinGroupList = append(updateNewCasbinGroupList, []string{"g_" + dpt.DepartmentID, "g_" + dpt.ParentDepartmentID})
		} else if !hasCasbinRule {
			addNewCasbinGroupList = append(addNewCasbinGroupList, []string{"g_" + dpt.DepartmentID, "g_" + dpt.ParentDepartmentID})
		}
	}
	for _, g := range gs.Items {
		v, ok := larkDptIds["g_"+g.SourceID]
		if ok && v || g.SourceID == "0" {
			continue
		}
		delIdsGroupList = append(delIdsGroupList, g.ID)
	}
	for _, cg := range larkCasbinGroups {
		v, ok := larkDptIds[cg[0]]
		if ok && v {
			continue
		}
		delCasbinGroupList = append(delCasbinGroupList, cg)
	}

	// casbin rules
	// add new groups
	if len(addNewCasbinGroupList) > 0 {
		_, err = e.AddNamedGroupingPolicies("g", addNewCasbinGroupList)
		if err != nil {
			log.Error(errors.Wrap(err, "Error when AddNamedGroupingPolicies.").Error())
			return err
		}
	}
	// update groups
	if len(updateOldCasbinGroupList) > 0 {
		_, err = e.UpdateNamedGroupingPolicies("g", updateOldCasbinGroupList, updateNewCasbinGroupList)
		if err != nil {
			log.Error(errors.Wrap(err, "Error when UpdateNamedGroupingPolicies.").Error())
			return err
		}
	}
	// delete groups
	if len(delCasbinGroupList) > 0 {
		_, err = e.RemoveNamedGroupingPolicies("g", delCasbinGroupList)
		if err != nil {
			log.Error(errors.Wrap(err, "Error when RemoveNamedGroupingPolicies.").Error())
			return err
		}
	}

	// update groups
	err = c.store.Groups().Sync(nil, &newGroupList, updateGroupList, delIdsGroupList)
	if err != nil {
		log.Error(err.Error())
		return errors.Wrap(err, "Error when sync groups into db, nothing will sync into db.")
	}

	log.Info("Success to sync lark contact groups.")

	return nil
}

func getLarkDpts(tat string, pageToken string) (*[]LarkDptItem, error) {
	var pageTokenQ string
	if pageToken != "" {
		pageTokenQ = "&page_token=" + pageToken
	}

	req, err := http.NewRequest("GET", LARK_OPEN_API_BASE+"/contact/v3/departments/0/children"+"?user_id_type=user_id&page_size=50&department_id_type=department_id&fetch_child=true"+pageTokenQ, nil)
	if err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when contruct request for lark department")
	}
	req.Header.Set("Authorization", "Bearer "+tat)
	resp, err := (&http.Client{}).Do(req)
	if err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when request lark department api")
	}
	defer resp.Body.Close()

	body, _ := ioutil.ReadAll(resp.Body)
	var res LarkCommonRes
	if err := json.Unmarshal(jsonc.ConvertJSON2Camel(body), &res); err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when unmarshal lark departments res")
	}
	if res.Code != 0 {
		log.Errorf("Lark API error code: %d, msg: %s", res.Code, res.Msg)
		return nil, errors.WithCode(code.ErrLarkApiBizErrorCode, "Lark Error Code: %d", res.Code)
	}

	var d LarkDptsData
	if err := json.Unmarshal(jsonc.ConvertJSON2Camel(res.Data), &d); err != nil {
		log.Error(err.Error())
		return nil, errors.Wrap(err, "Error when unmarshal lark departments res")
	}

	var validItems []LarkDptItem
	for _, it := range d.Items {
		if !it.Status.IsDeleted {
			validItems = append(validItems, it)
		}
	}

	var result []LarkDptItem
	result = append(result, validItems...)
	if d.HasMore && d.PageToken != "" {
		mItems, err := getLarkDpts(tat, d.PageToken)
		if err != nil {
			log.Error(err.Error())
			return nil, errors.Wrap(err, "Error when getLarkDpts for next page")
		}
		result = append(result, *mItems...)
	}

	return &result, nil
}
