package tour

import (
	"fmt"
	"strings"

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

// Options is retained for call-site compatibility. The deterministic fallback
// intentionally follows Understand Anything's generateHeuristicTour and does
// not apply project-specific ranking, localization, or step caps.
type Options struct {
	ProjectName    string
	MaxSteps       int
	OutputLanguage string
}

// Generate ports Understand Anything's generateHeuristicTour fallback. It
// topologically orders every non-concept node using every edge whose endpoints
// are code nodes, groups nodes by layer when present (otherwise batches by
// three), appends concepts as the final step, and assigns sequential orders.
func Generate(graph schema.KnowledgeGraph, _ Options) []schema.TourStep {
	conceptNodes := make([]schema.GraphNode, 0)
	codeNodes := make([]schema.GraphNode, 0)
	codeNodeIDs := make(map[string]struct{}, len(graph.Nodes))
	for _, node := range graph.Nodes {
		if node.Type == "concept" {
			conceptNodes = append(conceptNodes, node)
			continue
		}
		codeNodes = append(codeNodes, node)
		codeNodeIDs[node.ID] = struct{}{}
	}

	topoOrder := topologicalOrder(codeNodes, codeNodeIDs, graph.Edges)
	nodeByID := make(map[string]schema.GraphNode, len(graph.Nodes))
	for _, node := range graph.Nodes {
		nodeByID[node.ID] = node
	}

	steps := make([]schema.TourStep, 0)
	if len(graph.Layers) > 0 {
		steps = append(steps, layeredSteps(graph.Layers, topoOrder, nodeByID)...)
	} else {
		steps = append(steps, batchedSteps(topoOrder, nodeByID)...)
	}
	if len(conceptNodes) > 0 {
		steps = append(steps, conceptStep(conceptNodes))
	}
	for i := range steps {
		steps[i].Order = i + 1
	}
	return steps
}

func topologicalOrder(nodes []schema.GraphNode, nodeIDs map[string]struct{}, edges []schema.GraphEdge) []string {
	inDegree := make(map[string]int, len(nodes))
	adjacency := make(map[string][]string, len(nodes))
	nodeOrder := make([]string, 0, len(nodes))
	for _, node := range nodes {
		if _, exists := inDegree[node.ID]; !exists {
			nodeOrder = append(nodeOrder, node.ID)
		}
		inDegree[node.ID] = 0
		adjacency[node.ID] = []string{}
	}
	for _, edge := range edges {
		if _, ok := nodeIDs[edge.Source]; !ok {
			continue
		}
		if _, ok := nodeIDs[edge.Target]; !ok {
			continue
		}
		inDegree[edge.Target]++
		adjacency[edge.Source] = append(adjacency[edge.Source], edge.Target)
	}

	queue := make([]string, 0, len(nodeOrder))
	for _, nodeID := range nodeOrder {
		if inDegree[nodeID] == 0 {
			queue = append(queue, nodeID)
		}
	}

	order := make([]string, 0, len(nodes))
	seen := make(map[string]struct{}, len(nodes))
	for head := 0; head < len(queue); head++ {
		current := queue[head]
		order = append(order, current)
		seen[current] = struct{}{}
		for _, neighbor := range adjacency[current] {
			inDegree[neighbor]--
			if inDegree[neighbor] == 0 {
				queue = append(queue, neighbor)
			}
		}
	}
	for _, node := range nodes {
		if _, ok := seen[node.ID]; ok {
			continue
		}
		order = append(order, node.ID)
		seen[node.ID] = struct{}{}
	}
	return order
}

func layeredSteps(layers []schema.Layer, topoOrder []string, nodes map[string]schema.GraphNode) []schema.TourStep {
	nodeToLayer := make(map[string]string)
	for _, layer := range layers {
		for _, nodeID := range layer.NodeIDs {
			nodeToLayer[nodeID] = layer.ID
		}
	}

	layerOrder := make([]string, 0, len(layers))
	layerNodes := make(map[string][]string, len(layers))
	for _, nodeID := range topoOrder {
		layerID, ok := nodeToLayer[nodeID]
		if !ok {
			continue
		}
		if _, exists := layerNodes[layerID]; !exists {
			layerOrder = append(layerOrder, layerID)
		}
		layerNodes[layerID] = append(layerNodes[layerID], nodeID)
	}

	layerByID := make(map[string]schema.Layer, len(layers))
	for _, layer := range layers {
		layerByID[layer.ID] = layer
	}
	steps := make([]schema.TourStep, 0, len(layerOrder)+1)
	for _, layerID := range layerOrder {
		layer, ok := layerByID[layerID]
		nodeIDs := layerNodes[layerID]
		if !ok || len(nodeIDs) == 0 {
			continue
		}
		steps = append(steps, schema.TourStep{
			Title:       layer.Name,
			Description: layer.Description + ". Key files: " + nodeNames(nodeIDs, nodes, ", ") + ".",
			NodeIDs:     nodeIDs,
		})
	}

	layeredNodeIDs := make(map[string]struct{})
	for _, layer := range layers {
		for _, nodeID := range layer.NodeIDs {
			layeredNodeIDs[nodeID] = struct{}{}
		}
	}
	unlayered := make([]string, 0)
	for _, nodeID := range topoOrder {
		if _, ok := layeredNodeIDs[nodeID]; !ok {
			unlayered = append(unlayered, nodeID)
		}
	}
	if len(unlayered) > 0 {
		steps = append(steps, schema.TourStep{
			Title:       "Supporting Components",
			Description: "Additional supporting files: " + nodeNames(unlayered, nodes, ", ") + ".",
			NodeIDs:     unlayered,
		})
	}
	return steps
}

func batchedSteps(topoOrder []string, nodes map[string]schema.GraphNode) []schema.TourStep {
	steps := make([]schema.TourStep, 0, (len(topoOrder)+2)/3)
	for i := 0; i < len(topoOrder); i += 3 {
		end := i + 3
		if end > len(topoOrder) {
			end = len(topoOrder)
		}
		batch := append([]string(nil), topoOrder[i:end]...)
		summaries := make([]string, 0, len(batch))
		for _, id := range batch {
			node, ok := nodes[id]
			if !ok {
				summaries = append(summaries, id)
				continue
			}
			summaries = append(summaries, fmt.Sprintf("%s (%s)", node.Name, node.Summary))
		}
		steps = append(steps, schema.TourStep{
			Title:       fmt.Sprintf("Step %d: Code Walkthrough", i/3+1),
			Description: "Exploring: " + strings.Join(summaries, "; ") + ".",
			NodeIDs:     batch,
		})
	}
	return steps
}

func conceptStep(nodes []schema.GraphNode) schema.TourStep {
	summaries := make([]string, 0, len(nodes))
	ids := make([]string, 0, len(nodes))
	for _, node := range nodes {
		summaries = append(summaries, fmt.Sprintf("%s (%s)", node.Name, node.Summary))
		ids = append(ids, node.ID)
	}
	return schema.TourStep{
		Title:       "Key Concepts",
		Description: "Important architectural concepts: " + strings.Join(summaries, "; ") + ".",
		NodeIDs:     ids,
	}
}

func nodeNames(ids []string, nodes map[string]schema.GraphNode, separator string) string {
	names := make([]string, 0, len(ids))
	for _, id := range ids {
		if node, ok := nodes[id]; ok && node.Name != "" {
			names = append(names, node.Name)
		}
	}
	return strings.Join(names, separator)
}

func Normalize(steps []schema.TourStep, valid map[string]struct{}) []schema.TourStep {
	out := make([]schema.TourStep, 0, len(steps))
	for _, step := range steps {
		ids := filterNodeIDs(step.NodeIDs, valid)
		if len(ids) == 0 {
			continue
		}
		step.NodeIDs = ids
		step.Order = len(out) + 1
		if strings.TrimSpace(step.Title) == "" {
			step.Title = "Step"
		}
		if strings.TrimSpace(step.Description) == "" {
			step.Description = step.Title
		}
		out = append(out, step)
	}
	return out
}

func filterNodeIDs(ids []string, valid map[string]struct{}) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(ids))
	for _, id := range ids {
		if _, ok := valid[id]; !ok {
			continue
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out
}
