// Copyright 2026 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package modernize

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/token"
	"go/types"
	"slices"
	"strings"

	"golang.org/x/tools/go/analysis"
	"golang.org/x/tools/go/analysis/passes/inspect"
	"golang.org/x/tools/go/ast/edge"
	"golang.org/x/tools/go/ast/inspector"
	"golang.org/x/tools/internal/analysis/analyzerutil"
	typeindexanalyzer "golang.org/x/tools/internal/analysis/typeindex"
	"golang.org/x/tools/internal/astutil"
	"golang.org/x/tools/internal/moreiters"
	"golang.org/x/tools/internal/typesinternal/typeindex"
	"golang.org/x/tools/internal/versions"
)

var EmbedLitAnalyzer = &analysis.Analyzer{
	Name: "embedlit",
	Doc:  analyzerutil.MustExtractDoc(doc, "embedlit"),
	Requires: []*analysis.Analyzer{
		inspect.Analyzer,
		typeindexanalyzer.Analyzer,
	},
	Run: runEmbedLit,
	URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/modernize#embedlit",
}

// Go1.27 introduced the ability to directly access embedded struct fields.
// The embedlit modernizer suggests two types of fixes that use this feature:
// 1. Removing redundant field type specifiers in embedded struct fields.
// 2. Moving embedded struct field assignments inside of the struct literal
// initialization.
func runEmbedLit(pass *analysis.Pass) (any, error) {
	var (
		inspect = pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
		index   = pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
		info    = pass.TypesInfo
	)
	for curLit := range inspect.Root().Preorder((*ast.CompositeLit)(nil)) {
		if curLit.ParentEdgeKind() != edge.KeyValueExpr_Value { // non-nested comp lit
			// TODO(mkalil): Figure out how to handle addition/removal of commas in
			// the comp lit when we observe code where both patterns apply. (This will
			// likely require a significant amount of work). For now, only apply edits
			// from one pattern at a time.
			if !embedlitUnnest(pass, info, curLit) {
				err := embedlitCombine(pass, index, info, curLit) // calls pass.ReadFile
				if err != nil {
					return nil, err
				}
			}
		}
	}
	return nil, nil
}

// Pattern A: removing unneeded embedded field type specifier from the struct
// literal.
// T{U: U{f: v, ...}} => T{f: v, ...}
// It returns true if it reported a diagnostic with edits.
func embedlitUnnest(pass *analysis.Pass, info *types.Info, curLit inspector.Cursor) bool {
	var (
		edits       []analysis.TextEdit
		names       []string // names of the embedded field types that can be removed
		lit         = curLit.Node().(*ast.CompositeLit)
		compLitType = info.TypeOf(lit)
	)

	// checkLit determines whether any of the fields in the given struct literal can
	// be promoted, and calculates the corresponding edits.
	var checkLit func(lit *ast.CompositeLit)
	checkLit = func(lit *ast.CompositeLit) {
		for i, elt := range lit.Elts {
			// Can't promote an unkeyed field; would result in a syntax error.
			if kv, ok := elt.(*ast.KeyValueExpr); ok {
				if innerLit := isEmbeddedFieldLit(info, compLitType, kv); innerLit != nil {
					// Emit edits to delete the unnecessary embedded field type specifier
					// and its closing brace.
					closingPos := innerLit.Rbrace
					if len(innerLit.Elts) > 0 {
						// Delete any inner trailing commas or white space. Extra trailing commas
						// would result in invalid code.
						closingPos = innerLit.Elts[len(innerLit.Elts)-1].End()
					}
					file := astutil.EnclosingFile(curLit)
					// Enable modernizer only for Go1.27.
					if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
						return
					}
					// If any comments overlap with the range to delete, don't suggest a fix.
					if !moreiters.Empty(astutil.Comments(file, kv.Pos(), innerLit.Lbrace+1)) ||
						!moreiters.Empty(astutil.Comments(file, closingPos, innerLit.Rbrace+1)) {
						continue
					}
					// Delete starting from the key to the character right after the
					// opening brace of the inner literal:
					// T{U: U{f: v, ...}}
					//   -----
					startPos := kv.Pos()
					endPos := innerLit.Lbrace + 1

					// Delete the entire line if the key and its opening brace are
					// together on their own line. This prevents leaving behind unneeded
					// blank lines inside struct literals that `gofmt` will not remove.
					// T{
					// 		U: U{    <- delete entire line
					// 			f: v,
					//		}
					//	}
					//
					tokFile := pass.Fset.File(kv.Pos())
					lineOf := func(pos token.Pos) int {
						return tokFile.PositionFor(pos, false).Line
					}
					curLine := lineOf(kv.Pos())
					var prevLine int
					if i == 0 {
						// First element, so the previous line is the parent lbrace.
						prevLine = lineOf(lit.Lbrace)
					} else {
						prevLine = lineOf(lit.Elts[i-1].End())
					}

					// We can safely delete the entire line if the key value expression is
					// on a different line than the previous element, and the closing
					// brace of the inner literal is on a different line than its opening
					// brace.
					if prevLine < curLine && curLine < tokFile.LineCount() && // (1-based)
						lineOf(innerLit.Lbrace) < lineOf(innerLit.Rbrace) {
						lineStart := tokFile.LineStart(curLine)
						nextLineStart := tokFile.LineStart(curLine + 1)
						// Check that there are no comments on the line we are going to delete.
						if moreiters.Empty(astutil.Comments(file, lineStart, nextLineStart)) {
							startPos = tokFile.LineStart(curLine)
							endPos = nextLineStart
						}
					}

					edits = append(edits, []analysis.TextEdit{
						// T{U: U{f: v, ...}}
						//   -----         -
						{
							// Delete the key and the opening brace of the inner struct literal.
							Pos: startPos,
							End: endPos,
						},
						{
							// Delete the corresponding closing brace, including preceding
							// white space or commas. Failing to delete trailing commas may
							// result in invalid code.
							Pos: closingPos,
							End: innerLit.Rbrace + 1,
						},
					}...)
					names = append(names, kv.Key.(*ast.Ident).Name)
					checkLit(innerLit)
				}
			}
		}
	}
	checkLit(lit)
	if len(edits) > 0 {
		pass.Report(analysis.Diagnostic{
			Pos:     curLit.Node().Pos(),
			End:     curLit.Node().End(),
			Message: "embedded field type can be removed from struct literal",
			SuggestedFixes: []analysis.SuggestedFix{
				{
					Message:   fmt.Sprintf("Remove embedded field type%s %s", cond(len(names) == 1, "", "s"), strings.Join(names, ", ")),
					TextEdits: edits,
				},
			},
		})
		return true
	}
	return false
}

// Pattern B: moving embedded field assignments inside the struct literal
// initialization.
// t := T{...}; t.x = x => t := T{..., x: x}
// (or var t = ...)
func embedlitCombine(pass *analysis.Pass, index *typeindex.Index, info *types.Info, curLit inspector.Cursor) error {
	compLit := curLit.Node().(*ast.CompositeLit)
	if !moreiters.Every(slices.Values(compLit.Elts), func(e ast.Expr) bool {
		return is[*ast.KeyValueExpr](e)
	}) {
		// Promoting additional embedded fields would result in mixing keyed and
		// unkeyed fields, which isn't allowed.
		return nil
	}
	var (
		// Ident for "t" in the assignment.
		lhs *ast.Ident
		// The cursor representing the statement that initializes the comp lit "t".
		// We use its siblings to search for field assignments and verify that there
		// are no intervening statements, in case those statements observe "t".
		curStmt inspector.Cursor
	)
	switch curLit.ParentEdgeKind() {
	case edge.AssignStmt_Rhs:
		assign := curLit.Parent().Node().(*ast.AssignStmt)
		// TODO(mkalil): Handle lhs forms that aren't idents, i.e. x.y[i] = T{...}.
		if id, ok := assign.Lhs[curLit.ParentEdgeIndex()].(*ast.Ident); ok {
			lhs = id
			curStmt = curLit.Parent()
		}
	case edge.ValueSpec_Values:
		spec := curLit.Parent().Node().(*ast.ValueSpec)
		lhs = spec.Names[curLit.ParentEdgeIndex()]
		if decl, ok := moreiters.First(curLit.Enclosing((*ast.DeclStmt)(nil))); ok {
			curStmt = decl
		}
	default:
		return nil
	}

	if lhs == nil || !curStmt.Valid() {
		return nil
	}

	var (
		tObj = info.ObjectOf(lhs)
		// Marks the contiguous block of embedded field assign statements that will
		// be moved into the struct initialization.
		firstStmt, lastStmt inspector.Cursor
	)
stmtloop:
	for {
		var ok bool
		curStmt, ok = curStmt.NextSibling()
		if !ok {
			break // end of (e.g.) block
		}
		// All embedded field value assignments must immediately follow the struct
		// initialization.
		assign, ok := curStmt.Node().(*ast.AssignStmt)
		if !ok || len(assign.Lhs) != 1 || !(assign.Tok == token.ASSIGN || assign.Tok == token.DEFINE) {
			// TODO(mkalil): handle multi-assignments like t.x, t.y = 1, 2
			break
		}
		expr := assign.Lhs[0]
		sel, ok := expr.(*ast.SelectorExpr)
		if !ok {
			break
		}
		// Verify that sel.X refers to the same object as "t"
		selXId, ok := sel.X.(*ast.Ident)
		if !ok {
			// TODO(mkalil): handle deeply nested expressions like t.B.x
			break
		}
		obj := info.ObjectOf(selXId)
		if obj != tObj {
			break
		}
		rhsCur := curStmt.ChildAt(edge.AssignStmt_Rhs, 0)
		if uses(index, rhsCur, tObj) {
			break
		}
		for c := range rhsCur.Preorder((*ast.Ident)(nil)) {
			id := c.Node().(*ast.Ident)
			// If the rhs uses a value of t (e.g. t.x = t.y), don't suggest a fix because
			// we can't evaluate t.y when constructing the new literal.
			if info.ObjectOf(id) == tObj {
				break stmtloop
			}
			// Note: we don't need to worry about expressions with side effects
			// changing the behavior when moved inside the comp lit. The order of
			// effects will be preserved because we preserve the order of the key
			// value pairs inside the comp lit.
		}
		if !firstStmt.Valid() {
			firstStmt = curStmt
		}
		lastStmt = curStmt
	}

	if !firstStmt.Valid() {
		return nil
	}

	file := astutil.EnclosingFile(curLit)
	// Enable modernizer only for Go1.27.
	if !analyzerutil.FileUsesGoVersion(pass, file, versions.Go1_27) {
		return nil
	}

	// Read file content to determine if the struct lit has a trailing comma
	// after its last element.
	tokFile := pass.Fset.File(compLit.Rbrace)
	filename := tokFile.Name()
	src, err := pass.ReadFile(filename)
	if err != nil {
		return err
	}

	hasTrailingComma := false
	if len(compLit.Elts) > 0 {
		lastElt := compLit.Elts[len(compLit.Elts)-1]
		lastEltOffset := tokFile.Offset(lastElt.End())
		rbraceOffset := tokFile.Offset(compLit.Rbrace)
		hasTrailingComma = bytes.Contains(src[lastEltOffset:rbraceOffset], []byte(","))
	}
	var edits []analysis.TextEdit
	// Emit edits to move the field assignment into the struct lit while
	// removing it from its current place.
	// t := T{...}; t.x = v
	//           ----- --- -
	// t := T{...,    x:  v}

	// Add a trailing comma before the closing brace of compLit if one doesn't
	// exist, and delete the closing brace itself.
	// t := T{...}; t.x = v
	//           -
	// t := T{..., t.x = v
	if len(compLit.Elts) > 0 && !hasTrailingComma {
		edits = append(edits, analysis.TextEdit{
			Pos:     compLit.Rbrace,
			End:     compLit.Rbrace + 1,
			NewText: []byte(","),
		})
	} else {
		edits = append(edits, analysis.TextEdit{
			Pos: compLit.Rbrace,
			End: compLit.Rbrace + 1,
		})
	}

	// For each assignment:
	// t.x = v
	// -- ---
	//   x : v
	curStmt = firstStmt
	var prevStmt inspector.Cursor
	for {
		assign := curStmt.Node().(*ast.AssignStmt)
		expr := assign.Lhs[0]
		sel := expr.(*ast.SelectorExpr)
		// Delete "t."
		edits = append(edits, analysis.TextEdit{
			Pos: assign.Pos(),
			End: sel.Sel.Pos(),
		})
		// Replace "=" with ":"
		edits = append(edits, analysis.TextEdit{
			Pos:     expr.End(),
			End:     assign.TokPos + 1,
			NewText: []byte(":"),
		})

		// Add a comma after the previous assignment if this is not the first one.
		if prevStmt.Valid() {
			edits = append(edits, analysis.TextEdit{
				Pos:     prevStmt.Node().End(),
				NewText: []byte(","),
			})
		}

		// For the last assignment, add the closing brace of the struct lit.
		if curStmt == lastStmt {
			edits = append(edits, analysis.TextEdit{
				Pos:     assign.End(),
				NewText: []byte("}"),
			})
			break
		}
		prevStmt = curStmt
		curStmt, _ = curStmt.NextSibling() // can't fail because we break out of the loop when we hit lastStmt
	}

	pass.Report(analysis.Diagnostic{
		Pos:     curLit.Node().Pos(),
		End:     curLit.Node().End(),
		Message: "embedded field assignment can be moved to struct literal",
		SuggestedFixes: []analysis.SuggestedFix{
			{
				Message:   "Move embedded field assignment to struct literal",
				TextEdits: edits,
			},
		},
	})
	return nil
}

// isEmbeddedFieldLit determines whether elt is a KeyValueExpr "T: T{...}" for
// an embedded field for which we can safely remove its type.
// If so, it returns the corresponding CompositeLit.
// If elt contains an unkeyed field or ambiguous type, it returns nil.
func isEmbeddedFieldLit(info *types.Info, topLevelType types.Type, kv *ast.KeyValueExpr) *ast.CompositeLit {
	obj := keyedField(info, kv)
	if obj == nil || !obj.Embedded() {
		return nil
	}
	lit, ok := kv.Value.(*ast.CompositeLit)
	if !ok || len(lit.Elts) == 0 {
		// Skip if the struct literal is empty.
		return nil
	}
	// We cannot remove this type if any of its nested composite elements have
	// unkeyed fields or are ambiguous, so we check for those conditions before
	// returning.
	for _, elt := range lit.Elts {
		kv, ok := elt.(*ast.KeyValueExpr)
		if !ok {
			return nil
		}
		obj := keyedField(info, kv)
		if obj == nil {
			return nil
		}
		k := kv.Key.(*ast.Ident) // can't fail
		// Cannot promote an ambiguous type, for example:
		// type T struct { A; B }
		// type A struct { x int }
		// type B struct { x int }
		// _ = T{A: A{x: 1}}
		// cannot be simplified to T{x: 1} because T has two different embedded fields called "x".
		// We also reject composite literals with slice elements, as parentObj will be nil.
		parentObj, _, _ := types.LookupFieldOrMethod(topLevelType, true, obj.Pkg(), k.Name)
		if parentObj != obj {
			return nil
		}
	}
	return lit
}

// keyedField reports whether the key of kv is an embedded field type. If so, it
// returns the type of the embedded field, otherwise it returns nil.
func keyedField(info *types.Info, kv *ast.KeyValueExpr) *types.Var {
	k, ok := kv.Key.(*ast.Ident)
	if !ok {
		return nil
	}
	obj, ok := info.ObjectOf(k).(*types.Var)
	if !ok || !obj.IsField() {
		return nil
	}
	return obj
}
