// Copyright 2011-2019 Canonical Ltd
// Copyright 2025 The go-yaml Project Contributors
// SPDX-License-Identifier: Apache-2.0

// Struct metadata extraction for YAML marshaling/unmarshaling.
//
// This file analyzes Go struct types to build mappings between YAML keys and
// struct fields. It parses struct tags like `yaml:"name,omitempty,flow,inline"`
// and caches the results for efficient repeated access.
//
// Used by:
//   - Constructor: maps YAML keys to struct fields when unmarshaling
//   - Representer: maps struct fields to YAML keys when marshaling
//
// Key types:
//   - structInfo: cached metadata about a struct type
//   - fieldInfo: metadata about a single struct field
//   - getStructInfo(): analyzes a struct type and returns cached metadata

package libyaml

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
	"sync"
)

// structInfo holds cached information about a struct's YAML-relevant fields.
type structInfo struct {
	FieldsMap  map[string]fieldInfo
	FieldsList []fieldInfo

	// InlineMap is the number of the field in the struct that
	// contains an ,inline map, or -1 if there's none.
	InlineMap int

	// InlineConstructors holds indexes to inlined fields that
	// contain constructor values.
	InlineConstructors [][]int
}

// fieldInfo holds information about a single struct field.
type fieldInfo struct {
	Key       string
	Num       int
	OmitEmpty bool
	Flow      bool
	// Id holds the unique field identifier, so we can cheaply
	// check for field duplicates without maintaining an extra map.
	Id int

	// Inline holds the field index if the field is part of an inlined struct.
	Inline []int
}

// structMap caches struct reflection information.
// fieldMapMutex protects access to structMap.
// constructorType holds the [reflect.Type] for the constructor interface.
var (
	structMap       = make(map[reflect.Type]*structInfo)
	fieldMapMutex   sync.RWMutex
	constructorType reflect.Type
)

// constructor interface is defined here to detect types that implement
// UnmarshalYAML during struct reflection.
type constructor interface {
	UnmarshalYAML(value *Node) error
}

// init initializes the constructorType variable with the [reflect.Type] of constructor interface.
func init() {
	var v constructor
	constructorType = reflect.ValueOf(&v).Elem().Type()
}

// hasConstructYAMLMethod checks if a type has an UnmarshalYAML method
// that takes a *Node from an allowlisted v3 yaml package. This detects
// v3 backward-compatible Unmarshaler implementations whose Node type
// can't be checked via interface assertion from this package.
func hasConstructYAMLMethod(t reflect.Type) bool {
	method, found := t.MethodByName("UnmarshalYAML")
	if !found {
		return false
	}

	// Check signature: func(*T) UnmarshalYAML(*Node) error
	mtype := method.Type
	if mtype.NumIn() != 2 || mtype.NumOut() != 1 {
		return false
	}

	// First param is receiver (already checked by MethodByName)
	// Second param should be a pointer to a Node-like struct
	paramType := mtype.In(1)
	if paramType.Kind() != reflect.Ptr {
		return false
	}

	elemType := paramType.Elem()
	if elemType.Kind() != reflect.Struct || elemType.Name() != "Node" || !isYAMLNodePkg(elemType.PkgPath()) {
		return false
	}

	// Return type should be error
	retType := mtype.Out(0)
	if retType.Kind() != reflect.Interface || retType.Name() != "error" {
		return false
	}

	return true
}

func isYAMLNodePkg(pkg string) bool {
	switch pkg {
	case "gopkg.in/yaml.v3", "go.yaml.in/yaml/v3":
		return true
	}
	return false
}

// getStructInfo returns cached information about a struct type's fields.
// It parses struct tags and builds a map of field names to field info.
func getStructInfo(st reflect.Type) (*structInfo, error) {
	fieldMapMutex.RLock()
	sinfo, found := structMap[st]
	fieldMapMutex.RUnlock()
	if found {
		return sinfo, nil
	}

	n := st.NumField()
	fieldsMap := make(map[string]fieldInfo)
	fieldsList := make([]fieldInfo, 0, n)
	inlineMap := -1
	inlineConstructors := [][]int(nil)
	for i := 0; i != n; i++ {
		field := st.Field(i)
		if field.PkgPath != "" && !field.Anonymous {
			continue // Private field
		}

		info := fieldInfo{Num: i}

		tag := field.Tag.Get("yaml")
		if tag == "" && !strings.Contains(string(field.Tag), ":") {
			tag = string(field.Tag)
		}
		if tag == "-" {
			continue
		}

		inline := false
		fields := strings.Split(tag, ",")
		if len(fields) > 1 {
			for _, flag := range fields[1:] {
				switch flag {
				case "omitempty":
					info.OmitEmpty = true
				case "flow":
					info.Flow = true
				case "inline":
					inline = true
				default:
					return nil, fmt.Errorf("unsupported flag %q in tag %q of type %s", flag, tag, st)
				}
			}
			tag = fields[0]
		}

		if inline {
			switch field.Type.Kind() {
			case reflect.Map:
				if inlineMap >= 0 {
					return nil, errors.New("multiple ,inline maps in struct " + st.String())
				}
				if field.Type.Key() != reflect.TypeOf("") {
					return nil, errors.New("option ,inline needs a map with string keys in struct " + st.String())
				}
				inlineMap = info.Num
			case reflect.Struct, reflect.Pointer:
				ftype := field.Type
				for ftype.Kind() == reflect.Pointer {
					ftype = ftype.Elem()
				}
				if ftype.Kind() != reflect.Struct {
					return nil, errors.New("option ,inline may only be used on a struct or map field")
				}
				// Check for both libyaml.constructor and yaml.Unmarshaler (by method name)
				if reflect.PointerTo(ftype).Implements(constructorType) || hasConstructYAMLMethod(reflect.PointerTo(ftype)) {
					inlineConstructors = append(inlineConstructors, []int{i})
				} else {
					sinfo, err := getStructInfo(ftype)
					if err != nil {
						return nil, err
					}
					for _, index := range sinfo.InlineConstructors {
						inlineConstructors = append(inlineConstructors, append([]int{i}, index...))
					}
					for _, finfo := range sinfo.FieldsList {
						if _, found := fieldsMap[finfo.Key]; found {
							msg := "duplicated key '" + finfo.Key + "' in struct " + st.String()
							return nil, errors.New(msg)
						}
						if finfo.Inline == nil {
							finfo.Inline = []int{i, finfo.Num}
						} else {
							finfo.Inline = append([]int{i}, finfo.Inline...)
						}
						finfo.Id = len(fieldsList)
						fieldsMap[finfo.Key] = finfo
						fieldsList = append(fieldsList, finfo)
					}
				}
			default:
				return nil, errors.New("option ,inline may only be used on a struct or map field")
			}
			continue
		}

		if tag != "" {
			info.Key = tag
		} else {
			info.Key = strings.ToLower(field.Name)
		}

		if _, found = fieldsMap[info.Key]; found {
			msg := "duplicated key '" + info.Key + "' in struct " + st.String()
			return nil, errors.New(msg)
		}

		info.Id = len(fieldsList)
		fieldsList = append(fieldsList, info)
		fieldsMap[info.Key] = info
	}

	sinfo = &structInfo{
		FieldsMap:          fieldsMap,
		FieldsList:         fieldsList,
		InlineMap:          inlineMap,
		InlineConstructors: inlineConstructors,
	}

	fieldMapMutex.Lock()
	structMap[st] = sinfo
	fieldMapMutex.Unlock()
	return sinfo, nil
}
