// Copyright F3 Authors
// SPDX-License-Identifier: MIT

package generic

import (
	"context"
	"fmt"

	"code.forgejo.org/f3/gof3/v3/id"

	"code.forgejo.org/f3/gof3/v3/util"
)

type (
	MirrorUpsertFunc func(ctx context.Context, origin NodeInterface, originParent Path, destination NodeInterface, destinationParent Path)
	MirrorDeleteFunc func(ctx context.Context, destination NodeInterface, destinationParent Path)
)

type MirrorOptions struct {
	destinationTree TreeInterface
	listOptions     ListOptions
	upsert          MirrorUpsertFunc
	delete          MirrorDeleteFunc
}

func NewMirrorOptions(destinationTree TreeInterface) *MirrorOptions {
	return &MirrorOptions{
		destinationTree: destinationTree,
		listOptions:     NilListOptions,
	}
}

func (o *MirrorOptions) GetListOptions() ListOptions {
	return o.listOptions
}

func (o *MirrorOptions) SetListOptions(listOptions ListOptions) {
	o.listOptions = listOptions
}

func (o *MirrorOptions) SetUpsert(upsert MirrorUpsertFunc) *MirrorOptions {
	o.upsert = upsert
	return o
}

func (o *MirrorOptions) SetDelete(delete MirrorDeleteFunc) *MirrorOptions {
	o.delete = delete
	return o
}

func TreeUnifyPath(ctx context.Context, origin TreeInterface, p Path, destination TreeInterface, options *MirrorOptions) Path {
	if p.Empty() || p.First().GetID().String() == "." {
		return NewNodePathFromString("")
	}

	originRoot := origin.GetRoot()
	destinationRoot := destination.GetRoot()

	if destinationRoot == nil {
		destinationRoot = NodeFactory(ctx, destination, originRoot.GetKind())
		destination.SetRoot(destinationRoot)
	}

	p = p.RemoveFirst()

	if p.Empty() {
		return NewNodePathFromString("")
	}

	originNode := originRoot.GetChild(p.First().GetID())
	origin.Debug("origin '%s' | destination '%s'", ReadableString(originNode.GetCurrentPath()), ReadableString(destinationRoot.GetCurrentPath()))
	return NodeUnifyPath(ctx, originNode, NewPath(originRoot), p.RemoveFirst(), NewPath(destinationRoot), options)
}

func TreeUnify(ctx context.Context, origin, destination TreeInterface, options *MirrorOptions) {
	origin.Trace("")

	originRoot := origin.GetRoot()

	if originRoot == nil {
		destination.SetRoot(nil)
		return
	}

	destinationRoot := destination.GetRoot()

	if destinationRoot == nil {
		destinationRoot = NodeFactory(ctx, destination, originRoot.GetKind())
		destination.SetRoot(destinationRoot)
		NodeCopy(ctx, originRoot, destinationRoot, originRoot.GetID(), options)
	}

	NodeUnify(ctx, originRoot, NewPath(), destinationRoot, NewPath(), options)
}

func NodeCopy(ctx context.Context, origin, destination NodeInterface, destinationID id.NodeID, options *MirrorOptions) {
	prefix := fmt.Sprintf("origin '%s' | destination '%s'", ReadableString(origin.GetCurrentPath()), ReadableString(destination.GetCurrentPath()))
	origin.Debug(prefix)
	f := origin.ToFormat()
	RemapReferences(ctx, origin, f)
	f.SetID(destinationID.String())
	TreeRename(origin.GetTree(), origin.GetKind(), f)
	if destination.IsUpToDate(f) {
		origin.Trace("%s | destination upsert is not needed", prefix)
		return
	}
	origin.Trace("%s | destination upsert is needed", prefix)
	destination.FromFormat(f)
	destination.Upsert(ctx)
}

func NodeUnify(ctx context.Context, origin NodeInterface, originPath Path, destination NodeInterface, destinationPath Path, options *MirrorOptions) {
	origin.Debug("origin at '%s' | destination at '%s'", ReadableString(origin.GetCurrentPath()), ReadableString(destination.GetCurrentPath()))
	util.MaybeTerminate(ctx)

	originPath = originPath.Append(origin)
	destinationPath = destinationPath.Append(destination)

	listOptions := options.GetListOptions()
	List(ctx, destination, listOptions)
	originChildren := origin.GetChildrenList()
	existing := make(map[id.NodeID]any, len(originChildren))
	for _, originChild := range originChildren {
		destinationID := GetMappedID(ctx, originChild, destination, options)
		destinationChild := destination.GetChild(destinationID)
		createDestinationChild := destinationChild == NilNode
		if createDestinationChild {
			destinationChild = NodeFactory(ctx, options.destinationTree, originChild.GetKind())
			destinationChild.SetParent(destination)
		}

		NodeCopy(ctx, originChild, destinationChild, destinationID, options)

		if options.upsert != nil {
			options.upsert(ctx, originChild, originPath, destinationChild, destinationPath)
		}

		if createDestinationChild {
			destination.SetChild(destinationChild)
		}
		SetMappedID(ctx, originChild, destinationChild, options)

		existing[destinationChild.GetID()] = true

		NodeUnify(ctx, originChild, originPath, destinationChild, destinationPath, options)
	}

	if !listOptions.HasSince() || !origin.GetSupportedListOptions().Since() {
		destinationChildren := destination.GetChildrenList()
		for _, destinationChild := range destinationChildren {
			destinationID := destinationChild.GetID()
			if _, ok := existing[destinationID]; !ok {
				destinationChild.Delete(ctx)
				if options.delete != nil {
					options.delete(ctx, destinationChild, destinationPath)
				}
				destination.DeleteChild(destinationID)
			}
		}
	}
}

func SetMappedID(ctx context.Context, origin, destination NodeInterface, options *MirrorOptions) {
	origin.SetMappedID(destination.GetID())
}

func GetMappedID(ctx context.Context, origin, destinationParent NodeInterface, options *MirrorOptions) id.NodeID {
	if i := origin.GetMappedID(); i != id.NilID {
		return i
	}

	f := origin.ToFormat()
	TreeRename(origin.GetTree(), origin.GetKind(), f)
	return destinationParent.LookupMappedID(ctx, origin.GetID(), f)
}

func NodeUnifyOne(ctx context.Context, origin NodeInterface, originPath, path, destinationPath Path, options *MirrorOptions) NodeInterface {
	destinationParent := destinationPath.Last()
	destinationID := GetMappedID(ctx, origin, destinationParent, options)
	prefix := fmt.Sprintf("origin '%s' | destination '%s/%s'", ReadableString(origin.GetCurrentPath()), ReadableString(destinationPath), destinationID)
	origin.Debug(prefix)
	destination := destinationParent.GetChild(destinationID)
	addDestination := destination == NilNode
	if addDestination {
		destination = NodeFactory(ctx, options.destinationTree, origin.GetKind())
		destination.SetParent(destinationParent)
		if destinationID != id.NilID {
			destination.SetID(destinationID)
			destination.Get(ctx)
		}
	}

	NodeCopy(ctx, origin, destination, destinationID, options)

	if options.upsert != nil {
		options.upsert(ctx, origin, originPath, destination, destinationPath)
	}

	if addDestination {
		destinationParent.SetChild(destination)
		origin.Trace("%s | add child %s to destination", prefix, destination.GetID())
	}
	origin.SetMappedID(destination.GetID())

	return destination
}

func NodeUnifyPath(ctx context.Context, origin NodeInterface, originPath, path, destinationPath Path, options *MirrorOptions) Path {
	origin.Debug("origin at '%s/%s' | destination at '%s' | moving to '%s'", ReadableString(originPath), origin.GetID(), ReadableString(destinationPath), ReadableString(path))

	util.MaybeTerminate(ctx)

	if path.Empty() {
		destination := NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)
		return destinationPath.Append(destination)
	}

	id := path.First().GetID().String()

	if id == "." {
		NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)
		return destinationPath
	}

	if id == ".." {
		parent, originPath := originPath.Pop()
		_, destinationPath := destinationPath.Pop()
		NodeUnifyPath(ctx, parent, originPath, path.RemoveFirst(), destinationPath, options)
		return destinationPath
	}

	destination := NodeUnifyOne(ctx, origin, originPath, path, destinationPath, options)

	originPath = originPath.Append(origin)
	destinationPath = destinationPath.Append(destination)

	child := origin.GetChild(path.First().GetID())
	if child == NilNode {
		panic(NewError[ErrorNodeNotFound]("%s has no child with id %s", originPath.String(), path.First().GetID()))
	}

	return NodeUnifyPath(ctx, child, originPath, path.RemoveFirst(), destinationPath, options)
}

type ParallelApplyFunc func(ctx context.Context, origin, destination NodeInterface)

type ParallelApplyOptions struct {
	fun ParallelApplyFunc

	where ApplyWhere
}

func NewParallelApplyOptions(fun ParallelApplyFunc) *ParallelApplyOptions {
	return &ParallelApplyOptions{
		fun: fun,
	}
}

func (o *ParallelApplyOptions) SetWhere(where ApplyWhere) *ParallelApplyOptions {
	o.where = where
	return o
}

func TreePathRemap(ctx context.Context, origin TreeInterface, p Path, destination TreeInterface) Path {
	remappedPath := NewPath()
	remap := func(ctx context.Context, origin, destination NodeInterface) {
		remappedPath = destination.GetCurrentPath()
	}
	TreeParallelApply(ctx, origin, p, destination, NewParallelApplyOptions(remap))
	return remappedPath
}

func TreeParallelApply(ctx context.Context, origin TreeInterface, path Path, destination TreeInterface, options *ParallelApplyOptions) bool {
	if path.Empty() {
		return true
	}
	return NodeParallelApply(ctx, origin.GetRoot(), path.RemoveFirst(), destination.GetRoot(), options)
}

func NodeParallelApply(ctx context.Context, origin NodeInterface, path Path, destination NodeInterface, options *ParallelApplyOptions) bool {
	origin.Debug("origin at '%s' | destination at '%s' | moving to '%s'", ReadableString(origin.GetCurrentPath()), ReadableString(destination.GetCurrentPath()), ReadableString(path))

	util.MaybeTerminate(ctx)

	if path.Empty() {
		options.fun(ctx, origin, destination)
		return true
	}

	i := path.First().GetID().String()

	if i == "." {
		return NodeParallelApply(ctx, origin, path.RemoveFirst(), destination, options)
	}

	if i == ".." {
		return NodeParallelApply(ctx, origin.GetParent(), path.RemoveFirst(), destination.GetParent(), options)
	}

	if options.where == ApplyEachNode {
		options.fun(ctx, origin, destination)
	}

	originChild := origin.GetChild(path.First().GetID())
	if originChild == NilNode {
		origin.Trace("no child %s", path.First().GetID())
		return false
	}
	mappedID := originChild.GetMappedID()
	if mappedID == id.NilID {
		origin.Trace("%s no mapped", originChild.GetID())
		return false
	}
	destinationChild := destination.GetChild(mappedID)
	if destinationChild == NilNode {
		panic(NewError[ErrorNodeNotFound]("%s has no child with id %s", destination.String(), mappedID))
	}

	return NodeParallelApply(ctx, originChild, path.RemoveFirst(), destinationChild, options)
}
