package assembler

import (
	"path/filepath"
	"sort"
	"strings"

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

func NormalizeGraph(graph schema.KnowledgeGraph) schema.KnowledgeGraph {
	graph = schema.SanitizeGraph(graph)
	graph = schema.NormalizeAliases(graph)
	remap := map[string]string{}
	for i := range graph.Nodes {
		oldID := strings.TrimSpace(graph.Nodes[i].ID)
		canonical := schema.NormalizeNodeID(graph.Nodes[i])
		if canonical == "" {
			canonical = oldID
		}
		graph.Nodes[i].ID = canonical
		if oldID != "" && oldID != canonical {
			remap[oldID] = canonical
		}
	}
	applyRemap(&graph, remap)
	graph.Edges = fixTestedByEdges(graph.Edges, graph.Nodes)
	graph.Edges = append(graph.Edges, synthesizeTestedByEdges(graph.Nodes, graph.Edges)...)
	graph.Nodes = tagTestedProductionNodes(graph.Nodes, graph.Edges)
	graph.Nodes = dedupeNodes(graph.Nodes)
	valid := nodeIDSet(graph.Nodes)
	graph.Edges = dedupeEdges(filterDanglingEdges(graph.Edges, valid))
	for i := range graph.Layers {
		graph.Layers[i].NodeIDs = dedupeStrings(filterStrings(graph.Layers[i].NodeIDs, valid))
	}
	for i := range graph.Tour {
		graph.Tour[i].NodeIDs = dedupeStrings(filterStrings(graph.Tour[i].NodeIDs, valid))
	}
	sortGraph(&graph)
	return graph
}

func MergeFragments(base schema.KnowledgeGraph, fragments ...schema.KnowledgeGraph) schema.KnowledgeGraph {
	merged := base
	if merged.Nodes == nil {
		merged.Nodes = []schema.GraphNode{}
	}
	if merged.Edges == nil {
		merged.Edges = []schema.GraphEdge{}
	}
	if merged.Layers == nil {
		merged.Layers = []schema.Layer{}
	}
	if merged.Tour == nil {
		merged.Tour = []schema.TourStep{}
	}
	for _, fragment := range fragments {
		merged.Nodes = append(merged.Nodes, fragment.Nodes...)
		merged.Edges = append(merged.Edges, fragment.Edges...)
		merged.Layers = append(merged.Layers, fragment.Layers...)
		merged.Tour = append(merged.Tour, fragment.Tour...)
	}
	return NormalizeGraph(merged)
}

func applyRemap(graph *schema.KnowledgeGraph, remap map[string]string) {
	if len(remap) == 0 {
		return
	}
	for i := range graph.Edges {
		if target, ok := remap[graph.Edges[i].Source]; ok {
			graph.Edges[i].Source = target
		}
		if target, ok := remap[graph.Edges[i].Target]; ok {
			graph.Edges[i].Target = target
		}
	}
	for i := range graph.Layers {
		for j, id := range graph.Layers[i].NodeIDs {
			if target, ok := remap[id]; ok {
				graph.Layers[i].NodeIDs[j] = target
			}
		}
	}
	for i := range graph.Tour {
		for j, id := range graph.Tour[i].NodeIDs {
			if target, ok := remap[id]; ok {
				graph.Tour[i].NodeIDs[j] = target
			}
		}
	}
}

func dedupeNodes(nodes []schema.GraphNode) []schema.GraphNode {
	sort.SliceStable(nodes, func(i, j int) bool { return nodeSortKey(nodes[i]) < nodeSortKey(nodes[j]) })
	seen := map[string]struct{}{}
	out := make([]schema.GraphNode, 0, len(nodes))
	for _, node := range nodes {
		if node.ID == "" {
			continue
		}
		if _, ok := seen[node.ID]; ok {
			continue
		}
		seen[node.ID] = struct{}{}
		if node.Tags == nil {
			node.Tags = []string{}
		}
		out = append(out, node)
	}
	return out
}

func dedupeEdges(edges []schema.GraphEdge) []schema.GraphEdge {
	sort.SliceStable(edges, func(i, j int) bool { return edgeDedupeSortKey(edges[i]) < edgeDedupeSortKey(edges[j]) })
	seen := map[string]struct{}{}
	out := make([]schema.GraphEdge, 0, len(edges))
	for _, edge := range edges {
		key := edge.Source + "\x00" + edge.Target + "\x00" + edge.Type
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, edge)
	}
	return out
}

func filterDanglingEdges(edges []schema.GraphEdge, valid map[string]struct{}) []schema.GraphEdge {
	out := make([]schema.GraphEdge, 0, len(edges))
	for _, edge := range edges {
		if _, ok := valid[edge.Source]; !ok {
			continue
		}
		if _, ok := valid[edge.Target]; !ok {
			continue
		}
		out = append(out, edge)
	}
	return out
}

func fixTestedByEdges(edges []schema.GraphEdge, nodes []schema.GraphNode) []schema.GraphEdge {
	testNode := map[string]bool{}
	for _, node := range nodes {
		testNode[node.ID] = isTestNode(node)
	}
	out := make([]schema.GraphEdge, 0, len(edges))
	for _, edge := range edges {
		if edge.Type != "tested_by" {
			out = append(out, edge)
			continue
		}
		sourceTest := testNode[edge.Source]
		targetTest := testNode[edge.Target]
		switch {
		case sourceTest && !targetTest:
			edge.Source, edge.Target = edge.Target, edge.Source
		case !sourceTest && targetTest:
			// Already production -> test.
		default:
			// Avoid test<->test and prod<->prod tested_by edges.
			continue
		}
		edge.Direction = "forward"
		out = append(out, edge)
	}
	return out
}

func synthesizeTestedByEdges(nodes []schema.GraphNode, existing []schema.GraphEdge) []schema.GraphEdge {
	existingKeys := map[string]struct{}{}
	for _, edge := range existing {
		if edge.Type == "tested_by" {
			existingKeys[edge.Source+"\x00"+edge.Target] = struct{}{}
		}
	}
	testNodes := []schema.GraphNode{}
	productionNodes := []schema.GraphNode{}
	for _, node := range nodes {
		if isTestNode(node) {
			testNodes = append(testNodes, node)
		} else {
			productionNodes = append(productionNodes, node)
		}
	}
	out := []schema.GraphEdge{}
	for _, prod := range productionNodes {
		prodKey := productionKey(prod)
		if prodKey == "" {
			continue
		}
		for _, test := range testNodes {
			if productionKey(test) != prodKey {
				continue
			}
			key := prod.ID + "\x00" + test.ID
			if _, ok := existingKeys[key]; ok {
				continue
			}
			existingKeys[key] = struct{}{}
			out = append(out, schema.GraphEdge{
				Source:      prod.ID,
				Target:      test.ID,
				Type:        "tested_by",
				Direction:   "forward",
				Description: "Synthesized by production/test naming convention",
				Weight:      0.6,
			})
		}
	}
	return out
}

func tagTestedProductionNodes(nodes []schema.GraphNode, edges []schema.GraphEdge) []schema.GraphNode {
	tested := map[string]struct{}{}
	for _, edge := range edges {
		if edge.Type == "tested_by" {
			tested[edge.Source] = struct{}{}
		}
	}
	out := append([]schema.GraphNode(nil), nodes...)
	for i := range out {
		if _, ok := tested[out[i].ID]; !ok || isTestNode(out[i]) {
			continue
		}
		if !hasTag(out[i].Tags, "tested") {
			out[i].Tags = append(out[i].Tags, "tested")
			sort.Strings(out[i].Tags)
		}
	}
	return out
}

func isTestNode(node schema.GraphNode) bool {
	path := strings.ToLower(filepath.ToSlash(node.FilePath))
	name := strings.ToLower(node.Name)
	id := strings.ToLower(node.ID)
	if strings.Contains(path, "_test.") || strings.Contains(path, ".test.") || strings.Contains(path, ".spec.") ||
		strings.Contains(path, "/test/") || strings.Contains(path, "/tests/") || strings.HasPrefix(path, "test/") || strings.HasPrefix(path, "tests/") ||
		strings.Contains(name, "test") || strings.Contains(id, "_test.") || strings.Contains(id, ".test.") || strings.Contains(id, ".spec.") {
		return true
	}
	for _, tag := range node.Tags {
		if strings.EqualFold(tag, "test") || strings.EqualFold(tag, "tests") {
			return true
		}
	}
	return false
}

func productionKey(node schema.GraphNode) string {
	if node.ID == "" {
		return ""
	}
	path := normalizeTestPath(node.FilePath)
	switch node.Type {
	case "file", "config", "document", "service", "pipeline", "table", "schema", "resource", "endpoint":
		if path == "" {
			return ""
		}
		return "file:" + path
	case "function", "class":
		if path == "" || strings.TrimSpace(node.Name) == "" {
			return ""
		}
		return node.Type + ":" + path + ":" + normalizeTestName(node.Name)
	default:
		return ""
	}
}

func normalizeTestPath(path string) string {
	path = strings.ToLower(filepath.ToSlash(strings.TrimSpace(path)))
	if path == "" {
		return ""
	}
	dir, base := filepath.ToSlash(filepath.Dir(path)), filepath.Base(path)
	ext := filepath.Ext(base)
	stem := strings.TrimSuffix(base, ext)
	stem = strings.TrimSuffix(stem, "_test")
	stem = strings.TrimSuffix(stem, ".test")
	stem = strings.TrimSuffix(stem, ".spec")
	if dir == "." || dir == "" {
		return stem + ext
	}
	return dir + "/" + stem + ext
}

func normalizeTestName(name string) string {
	name = strings.TrimSpace(name)
	for _, prefix := range []string{"Test", "test", "Spec", "spec"} {
		if strings.HasPrefix(name, prefix) && len(name) > len(prefix) {
			name = strings.TrimPrefix(name, prefix)
			break
		}
	}
	for _, suffix := range []string{"Test", "Tests", "Spec", "Specs", "test", "tests", "spec", "specs"} {
		if strings.HasSuffix(name, suffix) && len(name) > len(suffix) {
			name = strings.TrimSuffix(name, suffix)
			break
		}
	}
	return strings.ToLower(name)
}

func hasTag(tags []string, wanted string) bool {
	for _, tag := range tags {
		if strings.EqualFold(tag, wanted) {
			return true
		}
	}
	return false
}

func nodeIDSet(nodes []schema.GraphNode) map[string]struct{} {
	set := map[string]struct{}{}
	for _, node := range nodes {
		if node.ID != "" {
			set[node.ID] = struct{}{}
		}
	}
	return set
}

func filterStrings(values []string, valid map[string]struct{}) []string {
	out := make([]string, 0, len(values))
	for _, value := range values {
		if _, ok := valid[value]; ok {
			out = append(out, value)
		}
	}
	return out
}

func dedupeStrings(values []string) []string {
	seen := map[string]struct{}{}
	out := make([]string, 0, len(values))
	for _, value := range values {
		if _, ok := seen[value]; ok {
			continue
		}
		seen[value] = struct{}{}
		out = append(out, value)
	}
	sort.Strings(out)
	return out
}

func sortGraph(graph *schema.KnowledgeGraph) {
	sort.Slice(graph.Nodes, func(i, j int) bool { return nodeSortKey(graph.Nodes[i]) < nodeSortKey(graph.Nodes[j]) })
	sort.Slice(graph.Edges, func(i, j int) bool { return edgeSortKey(graph.Edges[i]) < edgeSortKey(graph.Edges[j]) })
	sort.Slice(graph.Layers, func(i, j int) bool { return graph.Layers[i].ID < graph.Layers[j].ID })
	sort.Slice(graph.Tour, func(i, j int) bool { return graph.Tour[i].Order < graph.Tour[j].Order })
}

func nodeSortKey(node schema.GraphNode) string {
	return node.ID + "\x00" + node.Type + "\x00" + node.Name
}

func edgeSortKey(edge schema.GraphEdge) string {
	return edge.Source + "\x00" + edge.Target + "\x00" + edge.Type + "\x00" + edge.Direction
}

func edgeDedupeSortKey(edge schema.GraphEdge) string {
	return edge.Source + "\x00" + edge.Target + "\x00" + edge.Type + "\x00" + directionRank(edge.Direction) + "\x00" + edge.Direction
}

func directionRank(direction string) string {
	switch direction {
	case "forward":
		return "0"
	case "bidirectional":
		return "1"
	case "backward":
		return "2"
	default:
		return "3"
	}
}
