package knowledge

// RescoreGraphIntentFit recomputes node productFitScore using:
//  1. weighted intent similarity (product + research map)
//  2. graph proximity propagation from high-fit core nodes
//  3. tangential-topic penalty
func RescoreGraphIntentFit(graph *Graph, in PatrolTagInput) {
	if graph == nil || len(graph.Nodes) == 0 {
		return
	}
	profile := BuildIntentProfile(in)
	intentScores := make([]int, len(graph.Nodes))
	for i, node := range graph.Nodes {
		intentScores[i] = ScoreIntentSimilarity(NodeIntentText(node), profile)
	}
	propagated := propagateIntentAlongGraph(graph.Nodes, graph.Edges, intentScores)

	for i := range graph.Nodes {
		node := &graph.Nodes[i]
		semantic := intentScores[i]
		if propagated[i] > semantic {
			semantic = propagated[i]
		}
		llmScore := node.ProductFitScore
		if llmScore <= 0 {
			llmScore = defaultProductFit(node.NodeKind, node.Layer)
		}

		layerBlend := 0.5 + float64(minInt(node.Layer, 3))*0.08
		blended := int(float64(semantic)*layerBlend + float64(llmScore)*(1.0-layerBlend))
		if IsTangentialToIntent(NodeIntentText(*node), profile) {
			blended = minInt(blended, 28)
		}
		if blended < 0 {
			blended = 0
		}
		if blended > 100 {
			blended = 100
		}
		node.ProductFitScore = blended
	}
}

func propagateIntentAlongGraph(nodes []Node, edges []Edge, intentScores []int) []int {
	if len(nodes) == 0 {
		return nil
	}
	idIndex := map[string]int{}
	for i, node := range nodes {
		idIndex[node.ID] = i
	}
	adj := make([][]int, len(nodes))
	addEdge := func(from, to string) {
		fi, okFrom := idIndex[from]
		ti, okTo := idIndex[to]
		if !okFrom || !okTo || fi == ti {
			return
		}
		adj[fi] = append(adj[fi], ti)
		adj[ti] = append(adj[ti], fi)
	}
	for _, edge := range edges {
		addEdge(edge.From, edge.To)
	}

	out := make([]int, len(nodes))
	copy(out, intentScores)
	visited := make([]bool, len(nodes))
	type queueItem struct {
		idx  int
		dist int
	}
	queue := make([]queueItem, 0, len(nodes))
	for i, score := range intentScores {
		if score >= 55 || nodes[i].Layer == 0 {
			queue = append(queue, queueItem{idx: i, dist: 0})
			visited[i] = true
		}
	}
	for len(queue) > 0 {
		item := queue[0]
		queue = queue[1:]
		if item.dist >= 3 {
			continue
		}
		base := intentScores[item.idx]
		if base <= 0 {
			base = out[item.idx]
		}
		decayed := int(float64(base) * powDecay(0.74, item.dist+1))
		for _, next := range adj[item.idx] {
			if decayed > out[next] {
				out[next] = decayed
			}
			if !visited[next] {
				visited[next] = true
				queue = append(queue, queueItem{idx: next, dist: item.dist + 1})
			}
		}
	}
	return out
}

func powDecay(base float64, exp int) float64 {
	out := 1.0
	for i := 0; i < exp; i++ {
		out *= base
	}
	return out
}
