// this file discovers application-level configuration from cmd/syft/internal/options/ by parsing ecosystem config structs, their DescribeFields() methods, and default value functions.
package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"path/filepath"
	"reflect"
	"sort"
	"strings"
)

// AppConfigField represents an application-level configuration field for catalogers
type AppConfigField struct {
	Key          string // e.g., "golang.search-local-mod-cache-licenses"
	Description  string // extracted from DescribeFields() method
	DefaultValue any    // extracted from Default*() functions
}

// extractEcosystemConfigFieldsFromCatalog parses catalog.go and extracts the ecosystem-specific
// config fields from the Catalog struct, returning a map of struct type name to YAML tag
func extractEcosystemConfigFieldsFromCatalog(catalogFilePath string) (map[string]string, error) {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, catalogFilePath, nil, parser.ParseComments)
	if err != nil {
		return nil, fmt.Errorf("failed to parse catalog.go: %w", err)
	}

	// find the Catalog struct
	catalogStruct := findConfigStruct(f, "Catalog")
	if catalogStruct == nil {
		return nil, fmt.Errorf("catalog struct not found in %s", catalogFilePath)
	}

	// extract ecosystem config fields from the Catalog struct
	// these are between the "ecosystem-specific cataloger configuration" comment and the next section
	ecosystemConfigs := make(map[string]string)
	inEcosystemSection := false

	for _, field := range catalogStruct.Fields.List {
		// check for ecosystem section marker comment
		if field.Doc != nil {
			for _, comment := range field.Doc.List {
				if strings.Contains(comment.Text, "ecosystem-specific cataloger configuration") {
					inEcosystemSection = true
					break
				}
				// check if we've hit the next section (any comment marking a new section)
				if inEcosystemSection && strings.HasPrefix(comment.Text, "// configuration for") {
					inEcosystemSection = false
					break
				}
			}
		}

		if !inEcosystemSection {
			continue
		}

		// extract field type and yaml tag
		if len(field.Names) == 0 {
			continue
		}

		// get the type name (e.g., "golangConfig")
		var typeName string
		if ident, ok := field.Type.(*ast.Ident); ok {
			typeName = ident.Name
		} else {
			continue
		}

		// get the yaml tag
		yamlTag := extractYAMLTag(field)
		if yamlTag == "" || yamlTag == "-" {
			continue
		}

		ecosystemConfigs[typeName] = yamlTag
	}

	return ecosystemConfigs, nil
}

// findFilesWithCatalogerImports scans the options directory for .go files that import
// from "github.com/anchore/syft/syft/pkg/cataloger/*" packages
func findFilesWithCatalogerImports(optionsDir string) ([]string, error) {
	entries, err := os.ReadDir(optionsDir)
	if err != nil {
		return nil, fmt.Errorf("failed to read options directory: %w", err)
	}

	var candidateFiles []string
	for _, entry := range entries {
		if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") {
			continue
		}

		filePath := filepath.Join(optionsDir, entry.Name())

		// parse the file to check imports
		fset := token.NewFileSet()
		f, err := parser.ParseFile(fset, filePath, nil, parser.ImportsOnly)
		if err != nil {
			continue // skip files that can't be parsed
		}

		// check if file imports from cataloger packages
		for _, imp := range f.Imports {
			importPath := strings.Trim(imp.Path.Value, `"`)
			if strings.HasPrefix(importPath, "github.com/anchore/syft/syft/pkg/cataloger/") {
				candidateFiles = append(candidateFiles, filePath)
				break
			}
		}
	}

	return candidateFiles, nil
}

// extractConfigStructTypes parses a Go file and returns all struct type names defined in it
func extractConfigStructTypes(filePath string) ([]string, error) {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, filePath, nil, 0)
	if err != nil {
		return nil, fmt.Errorf("failed to parse %s: %w", filePath, err)
	}

	var structTypes []string
	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			// check if it's a struct type
			if _, ok := typeSpec.Type.(*ast.StructType); ok {
				structTypes = append(structTypes, typeSpec.Name.Name)
			}
		}
	}

	return structTypes, nil
}

// discoverCatalogerConfigs discovers cataloger config files by:
// 1. Finding files with cataloger imports in options directory
// 2. Extracting ecosystem config fields from Catalog struct
// 3. Matching file structs against Catalog fields
// Returns a map of file path to top-level YAML key and a reverse lookup map of YAML key to struct name
func discoverCatalogerConfigs(repoRoot string) (map[string]string, map[string]string, error) {
	optionsDir := filepath.Join(repoRoot, "cmd", "syft", "internal", "options")
	catalogFilePath := filepath.Join(optionsDir, "catalog.go")

	// get ecosystem config fields from Catalog struct
	ecosystemConfigs, err := extractEcosystemConfigFieldsFromCatalog(catalogFilePath)
	if err != nil {
		return nil, nil, err
	}

	if len(ecosystemConfigs) == 0 {
		return nil, nil, fmt.Errorf("no ecosystem config fields found in Catalog struct")
	}

	// find files with cataloger imports
	candidateFiles, err := findFilesWithCatalogerImports(optionsDir)
	if err != nil {
		return nil, nil, err
	}

	// match candidate files against Catalog ecosystem fields
	fileToKey := make(map[string]string)
	foundStructs := make(map[string]bool)

	for _, filePath := range candidateFiles {
		structTypes, err := extractConfigStructTypes(filePath)
		if err != nil {
			return nil, nil, err
		}

		// check if any struct type matches an ecosystem config
		for _, structType := range structTypes {
			if yamlKey, exists := ecosystemConfigs[structType]; exists {
				fileToKey[filePath] = yamlKey
				foundStructs[structType] = true
				break
			}
		}
	}

	// validate that all ecosystem configs were found
	var missingConfigs []string
	for structType := range ecosystemConfigs {
		if !foundStructs[structType] {
			missingConfigs = append(missingConfigs, structType)
		}
	}

	if len(missingConfigs) > 0 {
		sort.Strings(missingConfigs)
		return nil, nil, fmt.Errorf("could not find files for ecosystem configs: %s", strings.Join(missingConfigs, ", "))
	}

	// build reverse lookup map (yamlKey -> structName)
	keyToStruct := make(map[string]string)
	for structName, yamlKey := range ecosystemConfigs {
		keyToStruct[yamlKey] = structName
	}

	return fileToKey, keyToStruct, nil
}

// DiscoverAppConfigs discovers all application-level cataloger configuration fields
// from the options package
func DiscoverAppConfigs(repoRoot string) ([]AppConfigField, error) {
	// discover cataloger config files dynamically
	configFiles, keyToStruct, err := discoverCatalogerConfigs(repoRoot)
	if err != nil {
		return nil, fmt.Errorf("failed to discover cataloger configs: %w", err)
	}

	// extract configuration fields from each discovered file
	var configs []AppConfigField
	for filePath, topLevelKey := range configFiles {
		fields, err := extractAppConfigFields(filePath, topLevelKey, keyToStruct)
		if err != nil {
			return nil, fmt.Errorf("failed to extract config from %s: %w", filePath, err)
		}
		configs = append(configs, fields...)
	}

	// sort by key for consistent output
	sort.Slice(configs, func(i, j int) bool {
		return configs[i].Key < configs[j].Key
	})

	return configs, nil
}

// extractAppConfigFields extracts config fields from an options file
func extractAppConfigFields(filePath, topLevelKey string, keyToStruct map[string]string) ([]AppConfigField, error) {
	fset := token.NewFileSet()
	f, err := parser.ParseFile(fset, filePath, nil, parser.ParseComments)
	if err != nil {
		return nil, err
	}

	var configs []AppConfigField

	// find the main config struct (not nested ones)
	configStruct, descriptions := findAppConfigStructAndDescriptions(f, topLevelKey, keyToStruct)
	if configStruct == nil {
		return nil, fmt.Errorf("no config struct found in %s", filePath)
	}

	// extract default values from the default function
	defaults := extractAppDefaultValues(f)

	// build config fields from struct fields
	for _, field := range configStruct.Fields.List {
		// extract yaml tag to get the field key
		yamlKey := extractYAMLTag(field)
		if yamlKey == "" || yamlKey == "-" {
			continue
		}

		var fieldName string
		if len(field.Names) > 0 {
			fieldName = field.Names[0].Name
		} else {
			continue
		}

		// build full key path
		fullKey := topLevelKey + "." + yamlKey

		// handle nested structs (e.g., golang.MainModuleVersion)
		if isNestedStruct(field.Type) {
			nestedConfigs := extractNestedAppConfigs(f, fullKey, fieldName, field.Type, descriptions, defaults)
			configs = append(configs, nestedConfigs...)
			continue
		}

		// get description from DescribeFields
		description := descriptions[fieldName]

		// get default value
		defaultValue := defaults[fieldName]

		configs = append(configs, AppConfigField{
			Key:          fullKey,
			Description:  description,
			DefaultValue: defaultValue,
		})
	}

	return configs, nil
}

// findAppConfigStructAndDescriptions finds the main config struct and extracts field descriptions
// from the DescribeFields method
func findAppConfigStructAndDescriptions(f *ast.File, topLevelKey string, keyToStruct map[string]string) (*ast.StructType, map[string]string) {
	structName := keyToStruct[topLevelKey]
	configStruct := findConfigStruct(f, structName)
	descriptions := extractDescriptionsFromDescribeFields(f)
	return configStruct, descriptions
}

// findConfigStruct searches for the config struct with the expected name in the AST
func findConfigStruct(f *ast.File, expectedName string) *ast.StructType {
	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok {
				continue
			}

			structType, ok := typeSpec.Type.(*ast.StructType)
			if !ok {
				continue
			}

			if typeSpec.Name.Name == expectedName {
				return structType
			}
		}
	}
	return nil
}

// extractDescriptionsFromDescribeFields extracts field descriptions from the DescribeFields method
func extractDescriptionsFromDescribeFields(f *ast.File) map[string]string {
	descriptions := make(map[string]string)

	for _, decl := range f.Decls {
		funcDecl, ok := decl.(*ast.FuncDecl)
		if !ok || funcDecl.Name.Name != "DescribeFields" {
			continue
		}

		// extract descriptions from descriptions.Add calls
		ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
			callExpr, ok := n.(*ast.CallExpr)
			if !ok {
				return true
			}

			// check if this is a descriptions.Add call
			selector, ok := callExpr.Fun.(*ast.SelectorExpr)
			if !ok || selector.Sel.Name != "Add" {
				return true
			}

			// first argument should be a field reference (&o.FieldName or &o.Parent.FieldName)
			if len(callExpr.Args) < 2 {
				return true
			}

			fieldPath := extractFieldPathFromRef(callExpr.Args[0])
			if fieldPath == "" {
				return true
			}

			// second argument is the description string
			description := extractStringLiteral(callExpr.Args[1])
			if description != "" {
				description = cleanDescription(description)
				descriptions[fieldPath] = description
			}

			return true
		})
	}

	return descriptions
}

// extractNestedAppConfigs handles nested config structs like golang.MainModuleVersion
func extractNestedAppConfigs(f *ast.File, parentKey, parentFieldName string, fieldType ast.Expr, descriptions map[string]string, defaults map[string]any) []AppConfigField {
	var configs []AppConfigField

	// find the nested struct type
	var nestedStructName string
	switch t := fieldType.(type) {
	case *ast.Ident:
		nestedStructName = t.Name
	default:
		return nil
	}

	// find the struct definition
	var nestedStruct *ast.StructType
	for _, decl := range f.Decls {
		genDecl, ok := decl.(*ast.GenDecl)
		if !ok || genDecl.Tok != token.TYPE {
			continue
		}

		for _, spec := range genDecl.Specs {
			typeSpec, ok := spec.(*ast.TypeSpec)
			if !ok || typeSpec.Name.Name != nestedStructName {
				continue
			}

			var structOk bool
			nestedStruct, structOk = typeSpec.Type.(*ast.StructType)
			if structOk {
				break
			}
		}
	}

	if nestedStruct == nil {
		return nil
	}

	// extract fields from nested struct
	for _, field := range nestedStruct.Fields.List {
		yamlKey := extractYAMLTag(field)
		if yamlKey == "" || yamlKey == "-" {
			continue
		}

		var fieldName string
		if len(field.Names) > 0 {
			fieldName = field.Names[0].Name
		} else {
			continue
		}

		fullKey := parentKey + "." + yamlKey

		// get description using the nested path (e.g., "MainModuleVersion.FromLDFlags")
		nestedPath := parentFieldName + "." + fieldName
		description := descriptions[nestedPath]

		// try to get default value from nested defaults
		var defaultValue any
		if nestedDefaults, ok := defaults[parentFieldName].(map[string]any); ok {
			defaultValue = nestedDefaults[fieldName]
		}

		configs = append(configs, AppConfigField{
			Key:          fullKey,
			Description:  description,
			DefaultValue: defaultValue,
		})
	}

	return configs
}

// extractAppDefaultValues extracts default values from the default*Config function
func extractAppDefaultValues(f *ast.File) map[string]any {
	defaults := make(map[string]any)

	for _, decl := range f.Decls {
		funcDecl, ok := decl.(*ast.FuncDecl)
		if !ok || !strings.HasPrefix(funcDecl.Name.Name, "default") {
			continue
		}

		// look for return statements that construct the config struct
		ast.Inspect(funcDecl.Body, func(n ast.Node) bool {
			// look for return statements
			returnStmt, ok := n.(*ast.ReturnStmt)
			if !ok || len(returnStmt.Results) == 0 {
				return true
			}

			// check if returning a struct literal
			compositeLit, ok := returnStmt.Results[0].(*ast.CompositeLit)
			if !ok {
				return true
			}

			// extract field values from the composite literal
			for _, elt := range compositeLit.Elts {
				kvExpr, ok := elt.(*ast.KeyValueExpr)
				if !ok {
					continue
				}

				// get field name
				ident, ok := kvExpr.Key.(*ast.Ident)
				if !ok {
					continue
				}

				fieldName := ident.Name

				// extract the value
				value := extractAppValue(kvExpr.Value)
				if value != nil {
					defaults[fieldName] = value
				}
			}

			return true
		})
	}

	return defaults
}

// extractAppValue extracts a Go value from an AST expression
func extractAppValue(expr ast.Expr) any {
	switch v := expr.(type) {
	case *ast.BasicLit:
		// string, int, bool literals
		switch v.Kind {
		case token.STRING:
			return strings.Trim(v.Value, `"`)
		case token.INT:
			return v.Value
		case token.FLOAT:
			return v.Value
		}
	case *ast.Ident:
		// boolean values
		if v.Name == "true" {
			return true
		}
		if v.Name == "false" {
			return false
		}
		if v.Name == "nil" {
			return nil
		}
	case *ast.CompositeLit:
		// nested struct literal
		nested := make(map[string]any)
		for _, elt := range v.Elts {
			kvExpr, ok := elt.(*ast.KeyValueExpr)
			if !ok {
				continue
			}
			ident, ok := kvExpr.Key.(*ast.Ident)
			if !ok {
				continue
			}
			nested[ident.Name] = extractAppValue(kvExpr.Value)
		}
		if len(nested) > 0 {
			return nested
		}
	}
	return nil
}

// extractYAMLTag extracts the yaml tag value from a struct field
func extractYAMLTag(field *ast.Field) string {
	if field.Tag == nil {
		return ""
	}

	tag := strings.Trim(field.Tag.Value, "`")
	tags := reflect.StructTag(tag)
	yamlTag := tags.Get("yaml")

	// handle tags like "field-name,omitempty"
	if idx := strings.Index(yamlTag, ","); idx != -1 {
		yamlTag = yamlTag[:idx]
	}

	return yamlTag
}

// extractFieldPathFromRef extracts field path from & o.FieldName or &o.Parent.FieldName expression
func extractFieldPathFromRef(expr ast.Expr) string {
	unaryExpr, ok := expr.(*ast.UnaryExpr)
	if !ok {
		return ""
	}

	// handle nested field references like &o.MainModuleVersion.FromLDFlags
	var parts []string
	current := unaryExpr.X

	for {
		selectorExpr, ok := current.(*ast.SelectorExpr)
		if !ok {
			break
		}

		// add this selector to the path
		parts = append([]string{selectorExpr.Sel.Name}, parts...)

		// move to the next level
		current = selectorExpr.X
	}

	// join the parts with dots to create the full path
	// e.g., ["MainModuleVersion", "FromLDFlags"] -> "MainModuleVersion.FromLDFlags"
	if len(parts) > 0 {
		return strings.Join(parts, ".")
	}

	return ""
}

// extractStringLiteral extracts a string value from a BasicLit node
func extractStringLiteral(expr ast.Expr) string {
	lit, ok := expr.(*ast.BasicLit)
	if !ok || lit.Kind != token.STRING {
		return ""
	}

	// remove backticks or quotes
	value := strings.Trim(lit.Value, "`\"")
	return value
}

// cleanDescription cleans up multi-line descriptions
func cleanDescription(desc string) string {
	// replace multiple whitespace with single space
	desc = strings.Join(strings.Fields(desc), " ")
	return strings.TrimSpace(desc)
}

// isNestedStruct checks if a field type is a nested struct (not a pointer or basic type)
func isNestedStruct(expr ast.Expr) bool {
	switch t := expr.(type) {
	case *ast.Ident:
		// check if it's a struct type (not a basic type)
		// basic types would be: string, int, bool, etc.
		basicTypes := map[string]bool{
			"string": true, "int": true, "int8": true, "int16": true, "int32": true, "int64": true,
			"uint": true, "uint8": true, "uint16": true, "uint32": true, "uint64": true,
			"float32": true, "float64": true, "bool": true, "byte": true, "rune": true,
		}
		return !basicTypes[t.Name]
	case *ast.StarExpr:
		// pointer types are not nested structs for our purposes
		return false
	case *ast.ArrayType, *ast.MapType:
		return false
	default:
		return false
	}
}
