package graph_path_finder

import (
	"github.com/tech/sendico/payments/storage/model"
	"github.com/tech/sendico/pkg/discovery"
)

func shortestPath(
	source model.Rail,
	destination model.Rail,
	network string,
	adjacency map[model.Rail][]normalizedEdge,
) (*Path, bool) {
	best := map[model.Rail]score{
		source: {hops: 0, wildcardHops: 0, signature: string(source)},
	}
	parent := map[model.Rail]model.Rail{}
	parentEdge := map[model.Rail]normalizedEdge{}
	visited := map[model.Rail]bool{}

	for {
		current, ok := nextRail(best, visited)
		if !ok {
			break
		}
		if current == destination {
			break
		}
		visited[current] = true

		currentScore := best[current]
		for _, edge := range adjacency[current] {
			candidate := score{
				hops:         currentScore.hops + 1,
				wildcardHops: currentScore.wildcardHops,
				signature:    currentScore.signature + ">" + string(edge.to),
			}
			if network != "" && edge.network == "" {
				candidate.wildcardHops++
			}

			prev, seen := best[edge.to]
			if !seen || better(candidate, prev) {
				best[edge.to] = candidate
				parent[edge.to] = current
				parentEdge[edge.to] = edge
			}
		}
	}

	if _, ok := best[destination]; !ok {
		return nil, false
	}

	rails := []model.Rail{destination}
	edges := make([]Edge, 0, len(rails))
	for cursor := destination; cursor != source; {
		prev, hasParent := parent[cursor]
		edge, hasEdge := parentEdge[cursor]
		if !hasParent || !hasEdge {
			return nil, false
		}
		rails = append(rails, prev)
		edges = append(edges, Edge{
			FromRail: edge.from,
			ToRail:   edge.to,
			Network:  edge.network,
		})
		cursor = prev
	}
	reverseRails(rails)
	reverseEdges(edges)

	return &Path{
		Rails: rails,
		Edges: edges,
	}, true
}

func nextRail(best map[model.Rail]score, visited map[model.Rail]bool) (model.Rail, bool) {
	var selected model.Rail = discovery.RailUnspecified
	selectedScore := score{}
	found := false
	for rail, railScore := range best {
		if visited[rail] {
			continue
		}
		if !found || better(railScore, selectedScore) || (equal(railScore, selectedScore) && rail < selected) {
			found = true
			selected = rail
			selectedScore = railScore
		}
	}
	return selected, found
}

func better(left, right score) bool {
	if left.hops != right.hops {
		return left.hops < right.hops
	}
	if left.wildcardHops != right.wildcardHops {
		return left.wildcardHops < right.wildcardHops
	}
	return left.signature < right.signature
}

func equal(left, right score) bool {
	return left.hops == right.hops &&
		left.wildcardHops == right.wildcardHops &&
		left.signature == right.signature
}

func reverseRails(items []model.Rail) {
	for left, right := 0, len(items)-1; left < right; left, right = left+1, right-1 {
		items[left], items[right] = items[right], items[left]
	}
}

func reverseEdges(items []Edge) {
	for left, right := 0, len(items)-1; left < right; left, right = left+1, right-1 {
		items[left], items[right] = items[right], items[left]
	}
}
