package analysis

import (
	"sort"
	"strings"

	"github.com/zzet/gortex/internal/graph"
)

// edgePair identifies a directed edge between two nodes.
type edgePair struct{ from, to string }

// Cycle represents a detected dependency cycle in the graph.
type Cycle struct {
	Path     []string `json:"path"`     // ordered symbol IDs forming the cycle
	Kind     string   `json:"kind"`     // "import-cycle", "call-cycle", "cross-community-cycle"
	Severity int      `json:"severity"` // 3=import, 2=cross-community, 1=call
}

// DetectCycles finds all dependency cycles in the graph using Tarjan's SCC algorithm.
// If scope is non-empty, only nodes whose FilePath starts with scope are considered.
// Cycles are classified by edge type and community membership, then sorted by severity descending.
func DetectCycles(g *graph.Graph, communities *CommunityResult, scope string) []Cycle {
	nodes := g.AllNodes()
	edges := g.AllEdges()

	// Build set of in-scope node IDs
	inScope := make(map[string]bool, len(nodes))
	for _, n := range nodes {
		if n.Kind == graph.KindFile || n.Kind == graph.KindImport {
			continue
		}
		if scope != "" && !strings.HasPrefix(n.FilePath, scope) {
			continue
		}
		inScope[n.ID] = true
	}

	// Build adjacency list and track edge kinds between pairs
	adj := make(map[string][]string)
	edgeKinds := make(map[edgePair][]graph.EdgeKind)

	for _, e := range edges {
		if e.Kind != graph.EdgeImports && e.Kind != graph.EdgeCalls {
			continue
		}
		if !inScope[e.From] || !inScope[e.To] {
			continue
		}
		pair := edgePair{e.From, e.To}
		// Avoid duplicate adjacency entries
		if _, exists := edgeKinds[pair]; !exists {
			adj[e.From] = append(adj[e.From], e.To)
		}
		edgeKinds[pair] = append(edgeKinds[pair], e.Kind)
	}

	// Run Tarjan's SCC
	sccs := tarjanSCC(inScope, adj)

	// Convert SCCs to cycles
	var cycles []Cycle
	for _, scc := range sccs {
		if len(scc) < 2 {
			continue
		}

		// Order the cycle path by following edges within the SCC
		path := orderCyclePath(scc, adj)

		// Classify the cycle
		kind, severity := classifyCycle(path, edgeKinds, communities)

		cycles = append(cycles, Cycle{
			Path:     path,
			Kind:     kind,
			Severity: severity,
		})
	}

	// Sort by severity descending
	sort.Slice(cycles, func(i, j int) bool {
		return cycles[i].Severity > cycles[j].Severity
	})

	return cycles
}

// WouldCreateCycle checks if adding an edge from fromID to toID would create a cycle.
// It performs DFS from toID to see if fromID is reachable. If so, adding fromID→toID
// would close a cycle. Returns the cycle path from toID to fromID when found.
func WouldCreateCycle(g *graph.Graph, fromID, toID string) (bool, []string) {
	edges := g.AllEdges()

	// Build adjacency from calls and imports edges
	adj := make(map[string][]string)
	seen := make(map[string]map[string]bool)
	for _, e := range edges {
		if e.Kind != graph.EdgeImports && e.Kind != graph.EdgeCalls {
			continue
		}
		if seen[e.From] == nil {
			seen[e.From] = make(map[string]bool)
		}
		if !seen[e.From][e.To] {
			seen[e.From][e.To] = true
			adj[e.From] = append(adj[e.From], e.To)
		}
	}

	// DFS from toID looking for fromID
	visited := make(map[string]bool)
	parent := make(map[string]string)
	found := false

	var dfs func(node string)
	dfs = func(node string) {
		if found {
			return
		}
		if node == fromID {
			found = true
			return
		}
		visited[node] = true
		for _, next := range adj[node] {
			if found {
				return
			}
			if !visited[next] {
				parent[next] = node
				dfs(next)
			}
		}
	}

	parent[toID] = ""
	dfs(toID)

	if !found {
		return false, nil
	}

	// Reconstruct path from toID to fromID
	var path []string
	current := fromID
	for current != toID {
		path = append(path, current)
		current = parent[current]
	}
	path = append(path, toID)

	// Reverse to get toID → ... → fromID order
	for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
		path[i], path[j] = path[j], path[i]
	}

	return true, path
}

// tarjanSCC runs Tarjan's strongly connected components algorithm.
// Returns a list of SCCs, each being a slice of node IDs.
func tarjanSCC(nodeSet map[string]bool, adj map[string][]string) [][]string {
	index := 0
	stack := make([]string, 0)
	onStack := make(map[string]bool)
	indices := make(map[string]int)
	lowlinks := make(map[string]int)
	defined := make(map[string]bool)
	var sccs [][]string

	var strongConnect func(v string)
	strongConnect = func(v string) {
		indices[v] = index
		lowlinks[v] = index
		index++
		defined[v] = true
		stack = append(stack, v)
		onStack[v] = true

		for _, w := range adj[v] {
			if !nodeSet[w] {
				continue
			}
			if !defined[w] {
				strongConnect(w)
				if lowlinks[w] < lowlinks[v] {
					lowlinks[v] = lowlinks[w]
				}
			} else if onStack[w] {
				if indices[w] < lowlinks[v] {
					lowlinks[v] = indices[w]
				}
			}
		}

		// If v is a root node, pop the SCC
		if lowlinks[v] == indices[v] {
			var scc []string
			for {
				w := stack[len(stack)-1]
				stack = stack[:len(stack)-1]
				onStack[w] = false
				scc = append(scc, w)
				if w == v {
					break
				}
			}
			sccs = append(sccs, scc)
		}
	}

	for id := range nodeSet {
		if !defined[id] {
			strongConnect(id)
		}
	}

	return sccs
}

// orderCyclePath finds a valid cycle within the SCC members by following edges.
// The returned path guarantees that for every consecutive pair (path[i], path[i+1])
// and the closing pair (path[last], path[0]), a directed edge exists in adj.
// Note: the path may not include all SCC members if no Hamiltonian cycle exists.
func orderCyclePath(scc []string, adj map[string][]string) []string {
	sccSet := make(map[string]bool, len(scc))
	for _, id := range scc {
		sccSet[id] = true
	}

	// Find a cycle using DFS from the first node.
	// In an SCC with size >= 2, there is always at least one cycle.
	start := scc[0]
	visited := make(map[string]bool)
	parent := make(map[string]string)

	var cyclePath []string
	var dfs func(node string) bool
	dfs = func(node string) bool {
		visited[node] = true
		for _, next := range adj[node] {
			if !sccSet[next] {
				continue
			}
			if next == start && node != start {
				// Found a cycle back to start — reconstruct
				cyclePath = []string{start}
				cur := node
				var stack []string
				for cur != start {
					stack = append(stack, cur)
					cur = parent[cur]
				}
				// Reverse stack to get start -> ... -> node order
				for i := len(stack) - 1; i >= 0; i-- {
					cyclePath = append(cyclePath, stack[i])
				}
				return true
			}
			if !visited[next] {
				parent[next] = node
				if dfs(next) {
					return true
				}
			}
		}
		return false
	}

	parent[start] = ""
	dfs(start)

	if len(cyclePath) >= 2 {
		return cyclePath
	}

	// Fallback: return the SCC members in original order (should not happen for valid SCC >= 2)
	return scc
}

// classifyCycle determines the kind and severity of a cycle based on edge types
// and community membership.
func classifyCycle(path []string, edgeKinds map[edgePair][]graph.EdgeKind, communities *CommunityResult) (string, int) {
	// Check if any edge in the cycle is an import edge
	hasImport := false
	for i := 0; i < len(path); i++ {
		from := path[i]
		to := path[(i+1)%len(path)]
		pair := edgePair{from, to}
		for _, kind := range edgeKinds[pair] {
			if kind == graph.EdgeImports {
				hasImport = true
				break
			}
		}
		if hasImport {
			break
		}
	}

	if hasImport {
		return "import-cycle", 3
	}

	// Check if cycle spans multiple communities
	if communities != nil && communities.NodeToComm != nil {
		communitySet := make(map[string]bool)
		for _, id := range path {
			if comm, ok := communities.NodeToComm[id]; ok {
				communitySet[comm] = true
			}
		}
		if len(communitySet) > 1 {
			return "cross-community-cycle", 2
		}
	}

	return "call-cycle", 1
}
