package v1

import (
	"context"
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pingcap-inc/pchub/internal/apiserver/store"
	"github.com/pingcap-inc/pchub/internal/pkg/code"
	v1 "github.com/pingcap-inc/pchub/model/v1"
	"github.com/pingcap-inc/pchub/pkg/errors"
	"github.com/pingcap-inc/pchub/pkg/log"
)

type departmentService struct {
	store store.Factory
}

type DepartmentSrv interface {
	SyncWithLark(ctx context.Context) error
	ListAll(ctx context.Context) (*v1.DepartmentList, error)
	List(ctx context.Context, pid, excludes string, precision int) ([]*v1.Department, error)
	GetChainByLarkID(ctx context.Context, larkID string) (*[]v1.Department, error)
}

var _ DepartmentSrv = (*departmentService)(nil)

func newDepartment(srv *service) *departmentService {
	return &departmentService{store: srv.store}
}

func (d *departmentService) GetChainByLarkID(ctx context.Context, larkID string) (*[]v1.Department, error) {
	res, err := d.store.Department().GetChainByLarkID(ctx, larkID)
	if err != nil {
		log.L(ctx).Error(errors.Wrap(err, "Error when Department.GetChainByLarkID from db").Error())

		return nil, errors.Wrap(err, "Error when get department-chain from db.")
	}

	return res, nil
}

func (d *departmentService) SyncWithLark(ctx context.Context) error {
	log.L(ctx).Info("SyncWithLark (department) called.")

	commonService := &commonService{store: d.store}

	larkDpts, err := getLarkDpts(ctx, commonService, "")
	if err != nil {
		log.L(ctx).Error(err.Error())

		return errors.Wrap(err, "Error when get lark departments due to the error in getLarkDpts.")
	}

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

	dptTree := make(map[string][]LarkDptItem)
	for _, dp := range *larkDpts {
		dptTree[dp.ParentDepartmentID] = append(dptTree[dp.ParentDepartmentID], dp)
	}

	flatChildrenIDs := make(map[string][]string)
	dptLevels := make(map[string]int)
	flatDpts(&dptTree, "0", 0, []string{}, &flatChildrenIDs, &dptLevels)

	ds, err := d.store.Department().ListAll(ctx)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when Department.ListAll from db").Error())

			return errors.Wrap(err, "Error when list department from db.")
		}
	}

	nDS := make(map[string]*v1.Department)
	for _, d := range ds.Items {
		nDS[d.LarkID] = d
	}

	newDepartmentList, updateDepartmentList, delIdsDepartmentList := diffDepartments(larkDpts, &nDS, &flatChildrenIDs, ds, &dptLevels)

	// update departments
	err = d.store.Department().Sync(ctx, newDepartmentList, updateDepartmentList, delIdsDepartmentList)
	if err != nil {
		log.L(ctx).Error(err.Error())

		return errors.Wrap(err, "Error when sync department into db, nothing will sync into db.")
	}

	log.L(ctx).Info("Success to sync lark contact department.")

	return nil
}

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

type LarkDptItem struct {
	DepartmentID       string        `json:"department_id"`
	Name               string        `json:"name"`
	ParentDepartmentID string        `json:"parent_department_id"`
	Status             LarkDptStatus `json:"status"`
	OpenDepartmentID   string        `json:"open_department_id"`
}

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

func getLarkDpts(ctx context.Context, cs *commonService, pageToken string) (*[]LarkDptItem, error) {
	var pageTokenQ string
	if pageToken != "" {
		pageTokenQ = "&page_token=" + pageToken
	}

	dptsJsonRaw, err := cs.GetDataByReqLarkAPI(ctx, "GET", LarkOpenAPIBase+"/contact/v3/departments/0/children?user_id_type=user_id&page_size=50&department_id_type=department_id&fetch_child=true"+pageTokenQ, nil, nil)
	if err != nil {
		log.Error(err.Error())

		return nil, errors.Wrap(err, "Error when request lark departments api")
	}

	var d LarkDptsData
	if err = json.Unmarshal(dptsJsonRaw, &d); err != nil {
		log.L(ctx).Error(err.Error())

		return nil, errors.Wrap(err, "Error when unmarshal lark departments data")
	}

	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(ctx, cs, 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
}

func flatDpts(dptTree *map[string][]LarkDptItem, rootID string, level int, pids []string, flatChildrenIDs *map[string][]string, dptLevels *map[string]int) {
	children := (*dptTree)[rootID]
	(*dptLevels)[rootID] = level
	for _, c := range children {
		(*flatChildrenIDs)[rootID] = append((*flatChildrenIDs)[rootID], c.DepartmentID)
		for _, pid := range pids {
			(*flatChildrenIDs)[pid] = append((*flatChildrenIDs)[pid], c.DepartmentID)
		}

		flatDpts(dptTree, c.DepartmentID, level+1, append(pids, rootID), flatChildrenIDs, dptLevels)
	}
}

func diffDepartments(larkDpts *[]LarkDptItem, nDS *map[string]*v1.Department, flatChildrenIDs *map[string][]string, ds *v1.DepartmentList, dptLevels *map[string]int) (nds, uds []*v1.Department, dids []uint64) {
	larkDptIds := make(map[string]bool, len(*larkDpts))

	for _, dpt := range *larkDpts {
		if dpt.Status.IsDeleted {
			continue
		}

		gInDB, ok := (*nDS)[dpt.DepartmentID]
		childrenIDs := (*flatChildrenIDs)[dpt.DepartmentID]
		sort.SliceStable(childrenIDs, func(i, j int) bool {
			return childrenIDs[i] < childrenIDs[j]
		})
		childrenIDStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(childrenIDs)), ","), "[]")
		larkDptIds[dpt.DepartmentID] = true

		if ok {
			parentChange := dpt.ParentDepartmentID != gInDB.ParentLarkID
			openIDChange := dpt.OpenDepartmentID != gInDB.OpenLarkID
			childrenChange := childrenIDStr != gInDB.FlatChildrenLarkID
			levelChange := (*dptLevels)[dpt.DepartmentID] != gInDB.Level
			if dpt.Name != gInDB.Name || parentChange || childrenChange || openIDChange || levelChange {
				updateG := gInDB
				updateG.Name = dpt.Name
				updateG.ParentLarkID = dpt.ParentDepartmentID
				updateG.FlatChildrenLarkID = childrenIDStr
				updateG.OpenLarkID = dpt.OpenDepartmentID
				updateG.Level = (*dptLevels)[dpt.DepartmentID]
				uds = append(uds, updateG)
			}
		} else {
			newD := v1.Department{
				LarkID:             dpt.DepartmentID,
				ParentLarkID:       dpt.ParentDepartmentID,
				Name:               dpt.Name,
				FlatChildrenLarkID: childrenIDStr,
				CreatedAt:          time.Now(),
				Level:              (*dptLevels)[dpt.DepartmentID],
			}
			nds = append(nds, &newD)
		}
	}

	// always update root department
	updateRoot := (*nDS)["0"]
	childrenIDs := (*flatChildrenIDs)["0"]
	sort.SliceStable(childrenIDs, func(i, j int) bool {
		return childrenIDs[i] < childrenIDs[j]
	})
	childrenIDStr := strings.Trim(strings.Join(strings.Fields(fmt.Sprint(childrenIDs)), ","), "[]")
	updateRoot.FlatChildrenLarkID = childrenIDStr
	updateRoot.Level = 0
	uds = append(uds, updateRoot)

	for _, d := range ds.Items {
		v, ok := larkDptIds[d.LarkID]
		// handled or root department, do not delete
		if ok && v || d.LarkID == "0" {
			continue
		}
		dids = append(dids, d.ID)
	}

	return nds, uds, dids
}

func (d *departmentService) ListAll(ctx context.Context) (*v1.DepartmentList, error) {
	ds, err := d.store.Department().ListAll(ctx)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when Department.ListAll from db").Error())

			return nil, errors.Wrap(err, "Error when list department from db.")
		}
	}

	return ds, nil
}

func (d *departmentService) List(ctx context.Context, pid, excludes string, precision int) ([]*v1.Department, error) {
	ds, err := d.store.Department().List(ctx, pid, excludes, precision)
	if err != nil {
		coder := errors.ParseCoder(err)
		if coder.Code() != code.ErrRecordNotFound {
			log.L(ctx).Error(errors.Wrap(err, "Error when Department.List from db").Error())

			return nil, errors.Wrap(err, "Error when list department from db.")
		}
	}

	result := make([]*v1.Department, 0, len(ds))
	for _, d := range ds {
		if d.Level == precision {
			d.FlatChildrenLarkID = ""
		}
		result = append(result, d)
	}

	return result, nil
}
