package yqlib

import (
	"fmt"
	"strconv"
	"strings"
	"time"
)

func createAddOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
	return &ExpressionNode{Operation: &Operation{OperationType: addOpType},
		LHS: lhs,
		RHS: rhs}
}

func addAssignOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
	return compoundAssignFunction(d, context, expressionNode, createAddOp)
}

func toNodes(candidate *CandidateNode, lhs *CandidateNode) []*CandidateNode {
	if candidate.Tag == "!!null" {
		return []*CandidateNode{}
	}

	clone := candidate.Copy()

	switch candidate.Kind {
	case SequenceNode:
		return clone.Content
	default:
		if len(lhs.Content) > 0 {
			clone.Style = lhs.Content[0].Style
		}
		return []*CandidateNode{clone}
	}

}

func addOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
	log.Debugf("Add operator")

	return crossFunction(d, context.ReadOnlyClone(), expressionNode, add, false)
}

func add(_ *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
	lhsNode := lhs

	if lhsNode.Tag == "!!null" {
		return lhs.CopyAsReplacement(rhs), nil
	}

	target := lhs.CopyWithoutContent()

	switch lhsNode.Kind {
	case MappingNode:
		if rhs.Kind != MappingNode {
			return nil, fmt.Errorf("%v (%v) cannot be added to a %v (%v)", rhs.Tag, rhs.GetNicePath(), lhsNode.Tag, lhs.GetNicePath())
		}
		addMaps(target, lhs, rhs)
	case SequenceNode:
		addSequences(target, lhs, rhs)
	case ScalarNode:
		if rhs.Kind != ScalarNode {
			return nil, fmt.Errorf("%v (%v) cannot be added to a %v (%v)", rhs.Tag, rhs.GetNicePath(), lhsNode.Tag, lhs.GetNicePath())
		}
		target.Kind = ScalarNode
		target.Style = lhsNode.Style
		if err := addScalars(context, target, lhsNode, rhs); err != nil {
			return nil, err
		}
	}
	return target, nil
}

func addScalars(context Context, target *CandidateNode, lhs *CandidateNode, rhs *CandidateNode) error {
	lhsTag := lhs.Tag
	rhsTag := rhs.guessTagFromCustomType()
	lhsIsCustom := false
	if !strings.HasPrefix(lhsTag, "!!") {
		// custom tag - we have to have a guess
		lhsTag = lhs.guessTagFromCustomType()
		lhsIsCustom = true
	}

	isDateTime := lhs.Tag == "!!timestamp"

	// if the lhs is a string, it might be a timestamp in a custom format.
	if lhsTag == "!!str" && context.GetDateTimeLayout() != time.RFC3339 {
		_, err := parseDateTime(context.GetDateTimeLayout(), lhs.Value)
		isDateTime = err == nil
	}

	if isDateTime {
		return addDateTimes(context.GetDateTimeLayout(), target, lhs, rhs)

	} else if lhsTag == "!!str" {
		target.Tag = lhs.Tag
		if rhsTag == "!!null" {
			target.Value = lhs.Value
		} else {
			target.Value = lhs.Value + rhs.Value
		}

	} else if rhsTag == "!!str" {
		target.Tag = rhs.Tag
		target.Value = lhs.Value + rhs.Value
	} else if lhsTag == "!!int" && rhsTag == "!!int" {
		format, lhsNum, err := parseInt64(lhs.Value)
		if err != nil {
			return err
		}
		_, rhsNum, err := parseInt64(rhs.Value)
		if err != nil {
			return err
		}
		sum := lhsNum + rhsNum
		target.Tag = lhs.Tag
		target.Value = fmt.Sprintf(format, sum)
	} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
		lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
		if err != nil {
			return err
		}
		rhsNum, err := strconv.ParseFloat(rhs.Value, 64)
		if err != nil {
			return err
		}
		sum := lhsNum + rhsNum
		if lhsIsCustom {
			target.Tag = lhs.Tag
		} else {
			target.Tag = "!!float"
		}
		target.Value = fmt.Sprintf("%v", sum)
	} else {
		return fmt.Errorf("%v cannot be added to %v", lhsTag, rhsTag)
	}
	return nil
}

func addDateTimes(layout string, target *CandidateNode, lhs *CandidateNode, rhs *CandidateNode) error {

	duration, err := time.ParseDuration(rhs.Value)
	if err != nil {
		return fmt.Errorf("unable to parse duration [%v]: %w", rhs.Value, err)
	}

	currentTime, err := parseDateTime(layout, lhs.Value)
	if err != nil {
		return err
	}

	newTime := currentTime.Add(duration)
	target.Value = newTime.Format(layout)
	return nil

}

func addSequences(target *CandidateNode, lhs *CandidateNode, rhs *CandidateNode) {
	log.Debugf("adding sequences! target: %v; lhs %v; rhs: %v", NodeToString(target), NodeToString(lhs), NodeToString(rhs))
	target.Kind = SequenceNode
	if len(lhs.Content) == 0 {
		log.Debugf("dont copy lhs style")
		target.Style = 0
	}
	target.Tag = lhs.Tag

	extraNodes := toNodes(rhs, lhs)

	target.AddChildren(lhs.Content)
	target.AddChildren(extraNodes)
}

func addMaps(target *CandidateNode, lhsC *CandidateNode, rhsC *CandidateNode) {
	lhs := lhsC
	rhs := rhsC

	if len(lhs.Content) == 0 {
		log.Debugf("dont copy lhs style")
		target.Style = 0
	}

	target.Content = make([]*CandidateNode, 0)
	target.AddChildren(lhs.Content)

	for index := 0; index < len(rhs.Content); index = index + 2 {
		key := rhs.Content[index]
		value := rhs.Content[index+1]
		log.Debug("finding %v", key.Value)
		indexInLHS := findKeyInMap(target, key)
		log.Debug("indexInLhs %v", indexInLHS)
		if indexInLHS < 0 {
			// not in there, append it
			target.AddKeyValueChild(key, value)
		} else {
			// it's there, replace it
			oldValue := target.Content[indexInLHS+1]
			newValueCopy := oldValue.CopyAsReplacement(value)
			target.Content[indexInLHS+1] = newValueCopy
		}
	}
	target.Kind = MappingNode
	if len(lhs.Content) > 0 {
		target.Style = lhs.Style
	}
	target.Tag = lhs.Tag
}
