package utils

import (
	"context"
	"primesys_email/src/logger"
	"primesys_email/src/models"
	"primesys_email/src/models/division"
	"strings"
	"time"
)

// report copy is always sent to this address along with the root division
const reportCopyEmail = "report999.primesystrack@gmail.com"

// DivisionEmailNode is one division in the division hierarchy, with its sub-divisions as children.
type DivisionEmailNode struct {
	Division *division.DivisionLoginModel
	Children []*DivisionEmailNode
	// HasDevices is true when this division itself has devices, it gets its own report
	HasDevices bool
}

func NewDivisionEmailNode(d *division.DivisionLoginModel) *DivisionEmailNode {
	return &DivisionEmailNode{
		Division:   d,
		Children:   []*DivisionEmailNode{},
		HasDevices: hasDevices(d.DeviceList),
	}
}

// BuildTree links the divisions into a tree using their path (",rootId,parentId,currentId,").
// The division whose path has a single id is the root.
func BuildTree(entities []division.DivisionLoginModel) *DivisionEmailNode {

	logger.Info("built tree")

	if len(entities) == 0 {
		return nil
	}

	nodeMap := make(map[string]*DivisionEmailNode, len(entities))
	for i := range entities {
		// index into the slice so each node points to its own element, not a loop copy
		nodeMap[entities[i].ID.Hex()] = NewDivisionEmailNode(&entities[i])
	}

	var root *DivisionEmailNode

	for _, entity := range entities {

		ids := splitPath(entity.Path)

		if len(ids) == 0 {
			continue
		}

		if len(ids) == 1 {
			root = nodeMap[ids[0]]
			continue
		}

		parentId := ids[len(ids)-2]
		currentId := ids[len(ids)-1]

		parentNode := nodeMap[parentId]
		currentNode := nodeMap[currentId]

		if parentNode != nil && currentNode != nil {
			parentNode.Children = append(parentNode.Children, currentNode)
		}
	}

	// keep only the branches which end in a division with devices
	return pruneTree(root)
}

// pruneTree removes every branch which has no division with devices. It goes to the
// last child first: a last child without devices is dropped, and a parent is kept only
// when it still has children left or has devices itself. Returns nil when nothing is left.
func pruneTree(node *DivisionEmailNode) *DivisionEmailNode {
	if node == nil {
		return nil
	}

	kept := []*DivisionEmailNode{}
	for _, child := range node.Children {
		if prunedChild := pruneTree(child); prunedChild != nil {
			kept = append(kept, prunedChild)
		}
	}
	node.Children = kept

	if len(node.Children) == 0 && !node.HasDevices {
		return nil
	}

	return node
}

// splitPath returns the non blank ids of a path like ",id1,id2,"
func splitPath(path string) []string {
	var ids []string
	for id := range strings.SplitSeq(path, ",") {
		id = strings.TrimSpace(id)
		if id != "" {
			ids = append(ids, id)
		}
	}
	return ids
}

// DivisionEmailHandler does the DB logging and mail sending for the traversal.
// It is implemented in the service layer, utils cannot import repositories (import cycle).
type DivisionEmailHandler interface {
	StartLog(ctx context.Context, queueId string, divisionId string, d *division.DivisionLoginModel,
		processType models.ProcessType, userId string, deviceTypeId int, reportDate int64) *models.ReportEmailQueueLog
	FinishLog(ctx context.Context, entry *models.ReportEmailQueueLog, status models.ReportEmailStatus, sentTo string, errMsg string)
	SendParentEmail(ctx context.Context, d *division.DivisionLoginModel, deviceTypeId int, reportDate int64, userId string) (string, error)
	// GetReportLog returns the generated report of the division, nil when no report exists
	GetReportLog(ctx context.Context, d *division.DivisionLoginModel, deviceTypeId int, reportDate int64) *division.DivisionReportLog
	SendReportEmail(ctx context.Context, emailChain []string, reportLog *division.DivisionReportLog, d *division.DivisionLoginModel, userId string) (string, error)
}

// TraverseAndProcess sends the report to the root division and then walks its children.
func TraverseAndProcess(ctx context.Context, handler DivisionEmailHandler, root *DivisionEmailNode, deviceTypeId int, reportDate int64,
	divisionId string, userId string, queueId string) {

	if root == nil {
		return
	}

	var rootChain []string
	rootDivision := root.Division
	rootEmail := rootDivision.Username
	// only an explicit report_enable: false blocks, missing value is not blocked
	isBlocked := rootDivision.ReportEnable != nil && !*rootDivision.ReportEnable

	logger.Info("is this blocked %t", isBlocked)

	if strings.TrimSpace(rootEmail) != "" && !isBlocked {
		logEntry := handler.StartLog(ctx, queueId, divisionId, rootDivision, models.SendParentEmail, userId, deviceTypeId, reportDate)

		sentTo, err := handler.SendParentEmail(ctx, rootDivision, deviceTypeId, reportDate, userId)
		if err != nil {
			logger.Error("sendParentEmail failed for division [%s]: %v", rootDivision.Name, err)
			handler.FinishLog(ctx, logEntry, models.EmailFailed, "", err.Error())
		} else {
			logger.Info("mail sending")
			handler.FinishLog(ctx, logEntry, models.EmailSent, sentTo, "")
		}

		rootChain = append(rootChain, rootEmail, reportCopyEmail)
	}

	for _, child := range root.Children {
		processNode(ctx, handler, child, deviceTypeId, reportDate, divisionId, rootChain, userId, queueId)
	}
}

// processNode sends the division report to the division and every email above it in the
// hierarchy (parentEmailChain), then walks its children. A blocked division skips its whole subtree.
func processNode(ctx context.Context, handler DivisionEmailHandler, node *DivisionEmailNode, deviceTypeId int, reportDate int64,
	divisionId string, parentEmailChain []string, userId string, queueId string) {

	if node == nil {
		return
	}

	d := node.Division
	// only an explicit report_enable: false blocks, missing value is not blocked
	isBlocked := d.ReportEnable != nil && !*d.ReportEnable
	if isBlocked {
		return
	}

	// copy so the siblings do not share this node's email
	currentChain := make([]string, len(parentEmailChain), len(parentEmailChain)+1)
	copy(currentChain, parentEmailChain)

	nodeEmail := d.Username
	if strings.TrimSpace(nodeEmail) != "" {
		currentChain = append(currentChain, nodeEmail)
	}

	if hasDevices(d.DeviceList) {
		reportLog := handler.GetReportLog(ctx, d, deviceTypeId, reportDate)

		if reportLog != nil {
			if _, err := handler.SendReportEmail(ctx, currentChain, reportLog, d, userId); err != nil {
				logger.Error("sendReportEmail failed for division [%s]: %v", d.Name, err)
			}

			// gap between mails, stops early if the context is cancelled
			select {
			case <-ctx.Done():
				return
			case <-time.After(20 * time.Second):
			}
		}
	}

	for _, child := range node.Children {
		processNode(ctx, handler, child, deviceTypeId, reportDate, divisionId, currentChain, userId, queueId)
	}
}

// hasDevices reports whether the device list (",1,2,") has at least one device
func hasDevices(deviceList string) bool {
	for value := range strings.SplitSeq(deviceList, ",") {
		if strings.TrimSpace(value) != "" {
			return true
		}
	}
	return false
}

// LogTree prints the division hierarchy, one division per line, indented by level
func LogTree(node *DivisionEmailNode, level int) {
	if node == nil {
		return
	}

	d := node.Division
	logger.Info("%s- %s (%s) email: %s devices: %t children: %d",
		strings.Repeat("    ", level), d.Name, d.ID.Hex(), d.Username, node.HasDevices, len(node.Children))

	for _, child := range node.Children {
		LogTree(child, level+1)
	}
}
