package importgraph

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

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

type Graph struct {
	Files     []scanner.FileInfo
	Analyses  map[string]extractor.StructuralAnalysis
	ImportMap map[string][]ImportEdge
}

type ImportEdge struct {
	SourcePath string
	TargetPath string
	ImportPath string
}

func Build(files []scanner.FileInfo, analyses []extractor.StructuralAnalysis) Graph {
	graph := Graph{
		Files:     append([]scanner.FileInfo(nil), files...),
		Analyses:  map[string]extractor.StructuralAnalysis{},
		ImportMap: map[string][]ImportEdge{},
	}
	byPath := map[string]scanner.FileInfo{}
	goPackageDirs := map[string]string{}
	for _, file := range graph.Files {
		byPath[file.Path] = file
		if file.Language == "go" {
			dir := filepath.ToSlash(filepath.Dir(file.Path))
			if dir == "." {
				dir = ""
			}
			goPackageDirs[dir] = file.Path
		}
	}
	for _, analysis := range analyses {
		graph.Analyses[analysis.FilePath] = analysis
	}
	resolver := newResolver(byPath, goPackageDirs)
	for _, analysis := range analyses {
		edges := []ImportEdge{}
		for _, imp := range analysis.Imports {
			if target := resolver.resolve(analysis, imp.Path); target != "" {
				edges = append(edges, ImportEdge{SourcePath: analysis.FilePath, TargetPath: target, ImportPath: imp.Path})
			}
		}
		sort.Slice(edges, func(i, j int) bool {
			if edges[i].TargetPath == edges[j].TargetPath {
				return edges[i].ImportPath < edges[j].ImportPath
			}
			return edges[i].TargetPath < edges[j].TargetPath
		})
		graph.ImportMap[analysis.FilePath] = dedupeImportEdges(edges)
	}
	return graph
}

func resolveImport(sourcePath string, importPath string, byPath map[string]scanner.FileInfo, goPackageDirs map[string]string) string {
	return newResolver(byPath, goPackageDirs).resolve(extractor.StructuralAnalysis{FilePath: sourcePath, Language: "go"}, importPath)
}

type resolver struct {
	byPath        map[string]scanner.FileInfo
	goPackageDirs map[string]string
}

func newResolver(byPath map[string]scanner.FileInfo, goPackageDirs map[string]string) resolver {
	return resolver{byPath: byPath, goPackageDirs: goPackageDirs}
}

func (r resolver) resolve(analysis extractor.StructuralAnalysis, importPath string) string {
	if importPath == "" {
		return ""
	}
	importPath = strings.TrimSpace(importPath)
	language := strings.ToLower(strings.TrimSpace(analysis.Language))
	if language == "" {
		language = r.byPath[analysis.FilePath].Language
	}
	switch language {
	case "typescript", "javascript":
		if target := r.resolveCandidates(tsJSCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "python":
		if target := r.resolveCandidates(pythonCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "rust":
		if target := r.resolveCandidates(rustCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "java", "kotlin":
		if target := r.resolvePackageCandidates(dotPackageCandidates(importPath, languageExtensions(language))); target != "" {
			return target
		}
	case "csharp":
		if target := r.resolvePackageCandidates(dotPackageCandidates(importPath, []string{".cs"})); target != "" {
			return target
		}
	case "php":
		if target := r.resolveCandidates(phpCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "c", "cpp":
		if target := r.resolveCandidates(cCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "ruby":
		if target := r.resolveCandidates(rubyCandidates(analysis.FilePath, importPath)); target != "" {
			return target
		}
	case "protobuf":
		if target := r.resolveCandidates(exactAndRelativeCandidates(analysis.FilePath, importPath, []string{".proto"})); target != "" {
			return target
		}
	}
	return r.resolveGoish(analysis.FilePath, importPath)
}

func (r resolver) resolveGoish(sourcePath string, importPath string) string {
	candidates := []string{}
	if strings.HasPrefix(importPath, "./") || strings.HasPrefix(importPath, "../") {
		dir := filepath.Dir(sourcePath)
		candidates = append(candidates, filepath.ToSlash(filepath.Clean(filepath.Join(dir, importPath))))
	} else {
		parts := strings.Split(importPath, "/")
		start := 0
		if strings.Contains(importPath, ".") {
			// Module-qualified imports include a VCS/domain prefix. Resolve only
			// multi-segment repository-local suffixes so external packages such as
			// github.com/gin-gonic/gin do not accidentally match a local gin package.
			start = 1
		}
		for i := start; i < len(parts); i++ {
			candidate := strings.Join(parts[i:], "/")
			if start > 0 && !strings.Contains(candidate, "/") {
				continue
			}
			candidates = append(candidates, candidate)
		}
	}
	for _, candidate := range candidates {
		candidate = strings.Trim(candidate, "/")
		if candidate == "." {
			candidate = ""
		}
		if file, ok := r.byPath[candidate]; ok {
			return file.Path
		}
		for _, suffix := range []string{".go", "/index.go", "/main.go"} {
			if file, ok := r.byPath[candidate+suffix]; ok {
				return file.Path
			}
		}
		if file, ok := r.goPackageDirs[candidate]; ok {
			return file
		}
	}
	return ""
}

func (r resolver) resolveCandidates(candidates []string) string {
	for _, candidate := range candidates {
		candidate = cleanCandidate(candidate)
		if candidate == "" {
			continue
		}
		if file, ok := r.byPath[candidate]; ok {
			return file.Path
		}
	}
	return ""
}

func (r resolver) resolvePackageCandidates(candidates []string) string {
	if target := r.resolveCandidates(candidates); target != "" {
		return target
	}
	matches := []string{}
	for _, candidate := range candidates {
		candidate = cleanCandidate(candidate)
		if candidate == "" {
			continue
		}
		for path := range r.byPath {
			if strings.HasSuffix(path, "/"+candidate) {
				matches = append(matches, path)
			}
		}
	}
	sort.Strings(matches)
	if len(matches) == 0 {
		return ""
	}
	return r.byPath[matches[0]].Path
}

func tsJSCandidates(sourcePath string, importPath string) []string {
	extensions := []string{".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".vue"}
	return exactAndRelativeCandidates(sourcePath, importPath, extensions)
}

func pythonCandidates(sourcePath string, importPath string) []string {
	importPath = strings.TrimSpace(importPath)
	dots := 0
	for dots < len(importPath) && importPath[dots] == '.' {
		dots++
	}
	if dots > 0 {
		rest := strings.TrimPrefix(importPath[dots:], ".")
		dir := filepath.Dir(sourcePath)
		for i := 1; i < dots; i++ {
			dir = filepath.Dir(dir)
		}
		candidate := filepath.ToSlash(filepath.Join(dir, strings.ReplaceAll(rest, ".", "/")))
		return append(withExtensions(candidate, []string{".py"}), candidate+"/__init__.py")
	}
	candidate := strings.ReplaceAll(importPath, ".", "/")
	return append(withExtensions(candidate, []string{".py"}), candidate+"/__init__.py")
}

func rustCandidates(sourcePath string, importPath string) []string {
	path := strings.TrimSpace(importPath)
	path = strings.TrimPrefix(path, "crate::")
	path = strings.TrimPrefix(path, "self::")
	if strings.HasPrefix(path, "super::") {
		dir := filepath.Dir(sourcePath)
		for strings.HasPrefix(path, "super::") {
			path = strings.TrimPrefix(path, "super::")
			dir = filepath.Dir(dir)
		}
		candidate := filepath.ToSlash(filepath.Join(dir, strings.ReplaceAll(path, "::", "/")))
		return append(withExtensions(candidate, []string{".rs"}), candidate+"/mod.rs")
	}
	out := []string{}
	parts := strings.Split(path, "::")
	for n := len(parts); n >= 1; n-- {
		candidate := strings.ReplaceAll(strings.Join(parts[:n], "::"), "::", "/")
		out = append(out, withExtensions(candidate, []string{".rs"})...)
		out = append(out, candidate+"/mod.rs", "src/"+candidate+".rs", "src/"+candidate+"/mod.rs")
	}
	return out
}

func phpCandidates(sourcePath string, importPath string) []string {
	path := strings.ReplaceAll(strings.TrimSpace(importPath), "\\", "/")
	return exactAndRelativeCandidates(sourcePath, path, []string{".php"})
}

func cCandidates(sourcePath string, importPath string) []string {
	out := exactAndRelativeCandidates(sourcePath, importPath, []string{".h", ".hpp", ".hh", ".c", ".cc", ".cpp", ".cxx"})
	if !strings.HasPrefix(importPath, "./") && !strings.HasPrefix(importPath, "../") {
		dirCandidate := filepath.ToSlash(filepath.Join(filepath.Dir(sourcePath), importPath))
		out = append(out, withExtensions(dirCandidate, []string{".h", ".hpp", ".hh", ".c", ".cc", ".cpp", ".cxx"})...)
	}
	return out
}

func rubyCandidates(sourcePath string, importPath string) []string {
	if strings.HasPrefix(importPath, "./") || strings.HasPrefix(importPath, "../") {
		return exactAndRelativeCandidates(sourcePath, importPath, []string{".rb"})
	}
	return exactAndRelativeCandidates(sourcePath, strings.ReplaceAll(importPath, ".", "/"), []string{".rb"})
}

func dotPackageCandidates(importPath string, extensions []string) []string {
	candidate := strings.ReplaceAll(strings.TrimSpace(importPath), ".", "/")
	return withExtensions(candidate, extensions)
}

func exactAndRelativeCandidates(sourcePath string, importPath string, extensions []string) []string {
	out := []string{}
	importPath = strings.TrimSpace(importPath)
	if strings.HasPrefix(importPath, "./") || strings.HasPrefix(importPath, "../") {
		dir := filepath.Dir(sourcePath)
		base := filepath.ToSlash(filepath.Join(dir, importPath))
		out = append(out, withExtensions(base, extensions)...)
		out = append(out, indexCandidates(base, extensions)...)
		return out
	}
	out = append(out, withExtensions(importPath, extensions)...)
	out = append(out, indexCandidates(importPath, extensions)...)
	return out
}

func withExtensions(candidate string, extensions []string) []string {
	out := []string{candidate}
	ext := filepath.Ext(candidate)
	if ext != "" {
		return out
	}
	for _, suffix := range extensions {
		out = append(out, candidate+suffix)
	}
	return out
}

func indexCandidates(candidate string, extensions []string) []string {
	out := []string{}
	for _, suffix := range extensions {
		out = append(out, candidate+"/index"+suffix, candidate+"/main"+suffix)
	}
	return out
}

func languageExtensions(language string) []string {
	switch language {
	case "java":
		return []string{".java"}
	case "kotlin":
		return []string{".kt", ".kts"}
	default:
		return nil
	}
}

func cleanCandidate(candidate string) string {
	candidate = filepath.ToSlash(filepath.Clean(strings.Trim(candidate, "/")))
	if candidate == "." {
		return ""
	}
	return strings.TrimPrefix(candidate, "./")
}

func dedupeImportEdges(edges []ImportEdge) []ImportEdge {
	seen := map[string]struct{}{}
	out := make([]ImportEdge, 0, len(edges))
	for _, edge := range edges {
		key := edge.SourcePath + "\x00" + edge.TargetPath + "\x00" + edge.ImportPath
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		out = append(out, edge)
	}
	return out
}
