// Package analyzers provides custom Go analysis rules for FetchML code quality and security
package analyzers

import (
	"go/ast"

	"golang.org/x/tools/go/analysis"
)

// NoBareDetectorAnalyzer flags calls to GPUDetectorFactory.CreateDetector() that
// silently discard DetectionInfo needed for manifest and audit log.
// Use CreateDetectorWithInfo() instead to capture both the detector and its metadata.
var NoBareDetectorAnalyzer = &analysis.Analyzer{
	Name: "nobaredetector",
	Doc:  "flags bare CreateDetector calls that discard DetectionInfo",
	Run:  runNoBareDetector,
}

func runNoBareDetector(pass *analysis.Pass) (interface{}, error) {
	for _, file := range pass.Files {
		ast.Inspect(file, func(n ast.Node) bool {
			// Look for call expressions
			call, ok := n.(*ast.CallExpr)
			if !ok {
				return true
			}

			// Check if it's a method call like obj.CreateDetector(...)
			sel, ok := call.Fun.(*ast.SelectorExpr)
			if !ok {
				return true
			}

			// Check if the method name is CreateDetector
			if sel.Sel.Name != "CreateDetector" {
				return true
			}

			// Check if the result is assigned (which is okay)
			// If the parent is an assignment or short var decl, it's being captured
			if isAssigned(pass, call) {
				return true
			}

			// Check if the call is part of a selector chain that includes CreateDetectorWithInfo
			// This is a heuristic - if the same object has CreateDetectorWithInfo called on it,
			// we assume they're doing the right thing
			if hasCreateDetectorWithInfo(pass, call, sel) {
				return true
			}

			pass.Reportf(call.Pos(),
				"bare CreateDetector() call detected - use CreateDetectorWithInfo() to capture DetectionInfo for manifest/audit")

			return true
		})
	}
	return nil, nil
}

// isAssigned checks if a call expression's result is being assigned to a variable
func isAssigned(pass *analysis.Pass, call *ast.CallExpr) bool {
	// Walk up the AST to see if this call is the RHS of an assignment
	// This is a simplified check - in a full implementation, we'd use pass.TypesInfo
	// to track the parent node
	pos := call.Pos()
	for _, file := range pass.Files {
		if file.Pos() <= pos && pos <= file.End() {
			var found bool
			ast.Inspect(file, func(n ast.Node) bool {
				if n == nil {
					return true
				}
				if n.Pos() == pos {
					// Check if parent is an assignment or short var decl
					return false // Stop searching
				}
				// Check for assignment statement containing this call
				switch node := n.(type) {
				case *ast.AssignStmt:
					for _, rhs := range node.Rhs {
						if rhs.Pos() == pos {
							found = true
							return false
						}
					}
				case *ast.ValueSpec:
					for _, val := range node.Values {
						if val.Pos() == pos {
							found = true
							return false
						}
					}
				}
				return true
			})
			if found {
				return true
			}
		}
	}
	return false
}

// hasCreateDetectorWithInfo checks if the same object that has CreateDetector called
// also has CreateDetectorWithInfo called somewhere in the function
func hasCreateDetectorWithInfo(pass *analysis.Pass, call *ast.CallExpr, sel *ast.SelectorExpr) bool {
	// Get the object being called on (the receiver)
	receiverObj := sel.X

	// Find the containing function
	var containingFunc *ast.FuncDecl
	for _, file := range pass.Files {
		if file.Pos() <= call.Pos() && call.Pos() <= file.End() {
			ast.Inspect(file, func(n ast.Node) bool {
				if fn, ok := n.(*ast.FuncDecl); ok {
					if fn.Pos() <= call.Pos() && call.Pos() <= fn.End() {
						containingFunc = fn
						return false
					}
				}
				return true
			})
			break
		}
	}

	if containingFunc == nil {
		return false
	}

	// Check if CreateDetectorWithInfo is called on the same object in the same function
	found := false
	ast.Inspect(containingFunc, func(n ast.Node) bool {
		call2, ok := n.(*ast.CallExpr)
		if !ok {
			return true
		}
		sel2, ok := call2.Fun.(*ast.SelectorExpr)
		if !ok {
			return true
		}
		if sel2.Sel.Name != "CreateDetectorWithInfo" {
			return true
		}

		// Check if it's the same receiver object
		if isSameObject(receiverObj, sel2.X) {
			found = true
			return false
		}
		return true
	})

	return found
}

// isSameObject checks if two AST expressions refer to the same object
func isSameObject(a, b ast.Expr) bool {
	// Simple string comparison for identifiers and selectors
	// In a full implementation, we'd use types.Object from type checking
	aStr := exprToString(a)
	bStr := exprToString(b)
	return aStr == bStr
}

// exprToString converts an expression to a simple string representation
func exprToString(expr ast.Expr) string {
	switch e := expr.(type) {
	case *ast.Ident:
		return e.Name
	case *ast.SelectorExpr:
		return exprToString(e.X) + "." + e.Sel.Name
	default:
		return ""
	}
}
