package builder

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

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

type Options struct {
	Project schema.ProjectMeta
}

func Build(files []scanner.FileInfo, analyses []extractor.StructuralAnalysis, imports importgraph.Graph, options Options) schema.KnowledgeGraph {
	analysisByPath := map[string]extractor.StructuralAnalysis{}
	for _, analysis := range analyses {
		analysisByPath[analysis.FilePath] = analysis
	}
	graph := schema.KnowledgeGraph{
		Version: "1.0.0",
		Kind:    schema.KindCodebase,
		Project: options.Project,
		Nodes:   []schema.GraphNode{},
		Edges:   []schema.GraphEdge{},
		Layers:  []schema.Layer{},
		Tour:    []schema.TourStep{},
	}
	if graph.Project.Languages == nil {
		graph.Project.Languages = []string{}
	}
	if graph.Project.Frameworks == nil {
		graph.Project.Frameworks = []string{}
	}
	nodeIDs := map[string]struct{}{}
	functionIDs := map[string]string{}
	for _, file := range files {
		fileNode := fileNode(file)
		addNode(&graph, nodeIDs, fileNode)
		analysis := analysisByPath[file.Path]
		for _, typ := range analysis.Types {
			typeNode := typeNode(file.Path, analysis.Language, typ)
			addNode(&graph, nodeIDs, typeNode)
			addEdge(&graph, nodeIDs, containsEdge(fileNode.ID, typeNode.ID, 1))
		}
		for _, fn := range analysis.Functions {
			fnNode := functionNode(file.Path, analysis.Language, fn)
			addNode(&graph, nodeIDs, fnNode)
			functionIDs[file.Path+"\x00"+fn.Name] = fnNode.ID
			addEdge(&graph, nodeIDs, containsEdge(fileNode.ID, fnNode.ID, 1))
		}
		for _, sub := range analysis.SubNodes {
			subNode := analysisSubNode(file.Path, analysis.Language, sub)
			addNode(&graph, nodeIDs, subNode)
			addEdge(&graph, nodeIDs, containsEdge(fileNode.ID, subNode.ID, 0.95))
		}
	}
	for _, edges := range imports.ImportMap {
		for _, edge := range edges {
			addEdge(&graph, nodeIDs, schema.GraphEdge{Source: fileNodeID(edge.SourcePath, fileCategory(edge.SourcePath)), Target: fileNodeID(edge.TargetPath, fileCategory(edge.TargetPath)), Type: "imports", Direction: "forward", Weight: 0.7})
		}
	}
	for _, analysis := range analyses {
		for _, call := range analysis.Calls {
			source := functionIDs[analysis.FilePath+"\x00"+call.Caller]
			if source == "" {
				continue
			}
			target := resolveCallTarget(functionIDs, analysis.FilePath, call.Callee)
			if target == "" || source == target {
				continue
			}
			addEdge(&graph, nodeIDs, schema.GraphEdge{Source: source, Target: target, Type: "calls", Direction: "forward", Weight: 0.8})
		}
	}
	sort.Slice(graph.Nodes, func(i, j int) bool { return graph.Nodes[i].ID < graph.Nodes[j].ID })
	sort.Slice(graph.Edges, func(i, j int) bool { return edgeKey(graph.Edges[i]) < edgeKey(graph.Edges[j]) })
	result := schema.ValidateGraph(graph)
	if result.Success && result.Data != nil {
		return *result.Data
	}
	return graph
}

func fileNode(file scanner.FileInfo) schema.GraphNode {
	nodeType := fileCategoryNodeType(file.Category, file.Path)
	return schema.GraphNode{ID: fileNodeID(file.Path, file.Category), Type: nodeType, Name: filepath.Base(file.Path), FilePath: file.Path, Summary: summaryForFile(file), Tags: []string{string(file.Category)}, Complexity: complexityForLines(file.Lines)}
}

func fileNodeID(path string, category scanner.Category) string {
	return fileCategoryNodeType(category, path) + ":" + filepath.ToSlash(path)
}

func fileCategory(path string) scanner.Category {
	_, category := scanner.ClassifyPath(path)
	return category
}

func fileCategoryNodeType(category scanner.Category, path string) string {
	lower := strings.ToLower(path)
	switch category {
	case scanner.CategoryConfig:
		return "config"
	case scanner.CategoryDocs:
		return "document"
	case scanner.CategoryInfra:
		if strings.Contains(lower, ".github/workflows/") {
			return "pipeline"
		}
		return "resource"
	case scanner.CategoryScript:
		if base := strings.ToLower(filepath.Base(path)); base == "makefile" || base == "justfile" || strings.HasPrefix(base, "taskfile.") {
			return "pipeline"
		}
		return "file"
	default:
		return "file"
	}
}

func functionNode(path string, language string, fn extractor.Function) schema.GraphNode {
	lineRange := []int{fn.LineRange[0], fn.LineRange[1]}
	tags := []string{"function"}
	if language != "" {
		tags = append([]string{language}, tags...)
	}
	return schema.GraphNode{ID: "function:" + filepath.ToSlash(path) + ":" + fn.Name, Type: "function", Name: fn.Name, FilePath: path, LineRange: lineRange, Summary: deterministicFunctionSummary(fn), Tags: tags, Complexity: complexityForLineRange(lineRange)}
}

func deterministicFunctionSummary(fn extractor.Function) string {
	visibility := "Internal"
	if fn.Exported {
		visibility = "Exported"
	}
	if fn.Receiver != "" {
		return visibility + " method " + fn.Name + " on " + fn.Receiver + "."
	}
	return visibility + " function " + fn.Name + "."
}

func typeNode(path string, language string, typ extractor.Type) schema.GraphNode {
	lineRange := []int{typ.LineRange[0], typ.LineRange[1]}
	tags := []string{extractor.TypeNodeKind(typ.Kind)}
	if language != "" {
		tags = append([]string{language}, tags...)
	}
	return schema.GraphNode{ID: "class:" + filepath.ToSlash(path) + ":" + typ.Name, Type: "class", Name: typ.Name, FilePath: path, LineRange: lineRange, Summary: deterministicTypeSummary(typ), Tags: tags, Complexity: complexityForLineRange(lineRange)}
}

func deterministicTypeSummary(typ extractor.Type) string {
	visibility := "Internal"
	if typ.Exported {
		visibility = "Exported"
	}
	return visibility + " " + extractor.TypeNodeKind(typ.Kind) + " " + typ.Name + "."
}

func analysisSubNode(path string, language string, sub extractor.SubNode) schema.GraphNode {
	lineRange := []int{}
	if sub.LineRange[0] > 0 && sub.LineRange[1] > 0 {
		lineRange = []int{sub.LineRange[0], sub.LineRange[1]}
	}
	tags := append([]string(nil), sub.Tags...)
	if language != "" {
		tags = append([]string{language}, tags...)
	}
	return schema.GraphNode{
		ID:            strings.ToLower(strings.TrimSpace(sub.Type)) + ":" + filepath.ToSlash(path) + ":" + sub.Name,
		Type:          sub.Type,
		Name:          sub.Name,
		FilePath:      path,
		LineRange:     lineRange,
		Summary:       subNodeSummary(sub),
		Tags:          tags,
		Complexity:    complexityForLineRange(lineRange),
		LanguageNotes: sub.LanguageNotes,
	}
}

func subNodeSummary(sub extractor.SubNode) string {
	if strings.TrimSpace(sub.Summary) != "" {
		return strings.TrimSpace(sub.Summary)
	}
	return strings.TrimSpace(sub.Type + " " + sub.Name)
}

func containsEdge(source string, target string, weight float64) schema.GraphEdge {
	return schema.GraphEdge{Source: source, Target: target, Type: "contains", Direction: "forward", Weight: weight}
}

func addNode(graph *schema.KnowledgeGraph, nodeIDs map[string]struct{}, node schema.GraphNode) {
	if _, ok := nodeIDs[node.ID]; ok {
		return
	}
	nodeIDs[node.ID] = struct{}{}
	graph.Nodes = append(graph.Nodes, node)
}

func addEdge(graph *schema.KnowledgeGraph, nodeIDs map[string]struct{}, edge schema.GraphEdge) {
	if _, ok := nodeIDs[edge.Source]; !ok {
		return
	}
	if _, ok := nodeIDs[edge.Target]; !ok {
		return
	}
	for _, existing := range graph.Edges {
		if edgeKey(existing) == edgeKey(edge) {
			return
		}
	}
	graph.Edges = append(graph.Edges, edge)
}

func resolveCallTarget(functionIDs map[string]string, sourcePath string, callee string) string {
	if target := functionIDs[sourcePath+"\x00"+callee]; target != "" {
		return target
	}
	matches := []string{}
	for key, id := range functionIDs {
		if strings.HasSuffix(key, "\x00"+callee) {
			matches = append(matches, id)
		}
	}
	sort.Strings(matches)
	if len(matches) == 1 {
		return matches[0]
	}
	return ""
}

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

func summaryForFile(file scanner.FileInfo) string {
	if file.Language != "" {
		return file.Language + " " + string(file.Category) + " file"
	}
	return string(file.Category) + " file"
}

func complexityForLineRange(lineRange []int) string {
	if len(lineRange) == 2 {
		return complexityForLines(lineRange[1] - lineRange[0] + 1)
	}
	return "moderate"
}

func complexityForLines(lines int) string {
	switch {
	case lines <= 80:
		return "simple"
	case lines <= 240:
		return "moderate"
	default:
		return "complex"
	}
}
