// Copyright 2018 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 server

import (
	"context"
	"fmt"
	"slices"
	"strings"

	"golang.org/x/tools/gopls/internal/cache"
	"golang.org/x/tools/gopls/internal/file"
	"golang.org/x/tools/gopls/internal/golang"
	"golang.org/x/tools/gopls/internal/mod"
	"golang.org/x/tools/gopls/internal/protocol"
	"golang.org/x/tools/gopls/internal/protocol/command"
	"golang.org/x/tools/gopls/internal/settings"
	"golang.org/x/tools/internal/event"
)

func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) {
	ctx, done := event.Start(ctx, "server.CodeAction")
	defer done()

	fh, snapshot, release, err := s.session.FileOf(ctx, params.TextDocument.URI)
	if err != nil {
		return nil, err
	}
	defer release()
	uri := fh.URI()
	kind := snapshot.FileKind(fh)

	// Determine the supported code action kinds for this file.
	//
	// We interpret CodeActionKinds hierarchically, so refactor.rewrite
	// subsumes refactor.rewrite.change_quote, for example,
	// and "" (protocol.Empty) subsumes all kinds.
	// See ../protocol/codeactionkind.go for some code action theory.
	//
	// The Context.Only field specifies which code actions
	// the client wants. According to LSP 3.18 textDocument_codeAction,
	// an Only=[] should be interpreted as Only=["quickfix"]:
	//
	//   "In version 1.0 of the protocol, there weren’t any
	//   source or refactoring code actions. Code actions
	//   were solely used to (quick) fix code, not to
	//   write/rewrite code. So if a client asks for code
	//   actions without any kind, the standard quick fix
	//   code actions should be returned."
	//
	// However, this would deny clients (e.g. Vim+coc.nvim,
	// Emacs+eglot, and possibly others) the easiest and most
	// natural way of querying the server for the entire set of
	// available code actions. But reporting all available code
	// actions would be a nuisance for VS Code, since mere cursor
	// motion into a region with a code action (~anywhere) would
	// trigger a lightbulb usually associated with quickfixes.
	//
	// As a compromise, we use the trigger kind as a heuristic: if
	// the query was triggered by cursor motion (Automatic), we
	// respond with only quick fixes; if the query was invoked
	// explicitly (Invoked), we respond with all available
	// actions.
	codeActionKinds := make(map[protocol.CodeActionKind]bool)
	if len(params.Context.Only) > 0 {
		for _, kind := range params.Context.Only { // kind may be "" (=> all)
			codeActionKinds[kind] = true
		}
	} else {
		// No explicit kind specified.
		// Heuristic: decide based on trigger.
		if triggerKind(params) == protocol.CodeActionAutomatic {
			// e.g. cursor motion: show only quick fixes
			codeActionKinds[protocol.QuickFix] = true
		} else {
			// e.g. a menu selection (or unknown trigger kind,
			// as in our tests): show all available code actions.
			codeActionKinds[protocol.Empty] = true
		}
	}

	// enabled reports whether the specified kind of code action is required.
	enabled := func(kind protocol.CodeActionKind) bool {
		// Given "refactor.rewrite.foo", check for it,
		// then "refactor.rewrite", "refactor", then "".
		// A false map entry prunes the search for ancestors.
		//
		// If codeActionKinds contains protocol.Empty (""),
		// all kinds are enabled.
		for {
			if v, ok := codeActionKinds[kind]; ok {
				return v
			}
			if kind == "" {
				return false
			}

			// The "source.test" code action shouldn't be
			// returned to the client unless requested by
			// an exact match in Only.
			//
			// This mechanism exists to avoid a distracting
			// lightbulb (code action) on each Test function.
			// These actions are unwanted in VS Code because it
			// has Test Explorer, and in other editors because
			// the UX of executeCommand is unsatisfactory for tests:
			// it doesn't show the complete streaming output.
			// See https://github.com/joaotavora/eglot/discussions/1402
			// for a better solution. See also
			// https://github.com/golang/go/issues/67400.
			//
			// TODO(adonovan): consider instead switching on
			// codeActionTriggerKind. Perhaps other noisy Source
			// Actions should be guarded in the same way.
			if kind == settings.GoTest {
				return false // don't search ancestors
			}

			// Try the parent.
			if dot := strings.LastIndexByte(string(kind), '.'); dot >= 0 {
				kind = kind[:dot] // "refactor.foo" -> "refactor"
			} else {
				kind = "" // "refactor" -> ""
			}
		}
	}

	switch kind {
	case file.Mod:
		var actions []protocol.CodeAction

		fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, enabled)
		if err != nil {
			return nil, err
		}

		// Group vulnerability fixes by their range, and select only the most
		// appropriate upgrades.
		//
		// TODO(rfindley): can this instead be accomplished on the diagnosis side,
		// so that code action handling remains uniform?
		vulnFixes := make(map[protocol.Range][]protocol.CodeAction)
	searchFixes:
		for _, fix := range fixes {
			for _, diag := range fix.Diagnostics {
				if diag.Source == string(cache.Govulncheck) || diag.Source == string(cache.Vulncheck) {
					vulnFixes[diag.Range] = append(vulnFixes[diag.Range], fix)
					continue searchFixes
				}
			}
			actions = append(actions, fix)
		}

		for _, fixes := range vulnFixes {
			fixes = mod.SelectUpgradeCodeActions(fixes)
			actions = append(actions, fixes...)
		}

		return actions, nil

	case file.Go:
		// diagnostic-bundled code actions
		//
		// The diagnostics already have a UI presence (e.g. squiggly underline);
		// the associated action may additionally show (in VS Code) as a lightbulb.
		// Note s.codeActionsMatchingDiagnostics returns only fixes
		// detected during the analysis phase. golang.CodeActions computes
		// extra changes that can address some diagnostics.
		actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, enabled)
		if err != nil {
			return nil, err
		}

		// computed code actions (may include quickfixes from diagnostics)
		moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, enabled, triggerKind(params))
		if err != nil {
			return nil, err
		}
		actions = append(actions, moreActions...)

		// Don't suggest most fixes for generated files, since they are generally
		// not useful and some editors may apply them automatically on save.
		// (Unfortunately there's no reliable way to distinguish fixes from
		// queries, so we must list all kinds of queries here.)
		//
		// We make an exception for OrganizeImports, because
		// (a) it is needed when making temporary experimental
		//     changes (e.g. adding logging) in generated files, and
		// (b) it doesn't report diagnostics on well-formed code, and
		//     unedited generated files must be well formed.
		if golang.IsGenerated(ctx, snapshot, uri) {
			actions = slices.DeleteFunc(actions, func(a protocol.CodeAction) bool {
				switch a.Kind {
				case settings.GoTest,
					settings.GoDoc,
					settings.GoFreeSymbols,
					settings.GoSplitPackage,
					settings.GoAssembly,
					settings.GoplsDocFeatures,
					settings.GoToggleCompilerOptDetails:
					return false // read-only query
				case settings.OrganizeImports:
					return false // fix allowed in generated files (see #73959)
				}
				return true // potential write operation
			})
		}

		return actions, nil

	default:
		// Unsupported file kind for a code action.
		return nil, nil
	}
}

func triggerKind(params *protocol.CodeActionParams) protocol.CodeActionTriggerKind {
	if kind := params.Context.TriggerKind; kind != nil { // (some clients omit it)
		return *kind
	}
	return protocol.CodeActionUnknownTrigger
}

// ResolveCodeAction resolves missing Edit information (that is, computes the
// details of the necessary patch) in the given code action using the provided
// Data field of the CodeAction, which should contain the raw json of a protocol.Command.
//
// This should be called by the client before applying code actions, when the
// client has code action resolve support.
//
// This feature allows capable clients to preview and selectively apply the diff
// instead of applying the whole thing unconditionally through workspace/applyEdit.
func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) (*protocol.CodeAction, error) {
	ctx, done := event.Start(ctx, "server.ResolveCodeAction")
	defer done()

	// Only resolve the code action if there is Data provided.
	var cmd protocol.Command
	if ca.Data != nil {
		if err := protocol.UnmarshalJSON(*ca.Data, &cmd); err != nil {
			return nil, err
		}
	}
	ca.Data = nil
	if cmd.Command != "" {
		// Try to resolve codeaction edits by running the command.
		//
		// Unfortunately, we can't simply report failure to execute the command, as
		// some clients (namely, VS Code with the Live Share extension) eagerly
		// resolve *all* code actions, and returning an error leads to spammy
		// popups. Instead, we simply set the command on the code action, and let
		// it fail if/when invoked (golang/go#75442).
		//
		// This is arguably a client-side bug, but we work around it as it causes
		// significant problems for our users.
		// https://github.com/microsoft/live-share/issues/5293
		params := &protocol.ExecuteCommandParams{
			Command:   cmd.Command,
			Arguments: cmd.Arguments,
		}
		handler := &commandHandler{
			s:      s,
			params: params,
		}
		edit, err := command.Dispatch(ctx, params, handler)
		if err != nil {
			// Resolving edits failed, so just resolve the command.
			ca.Command = &cmd
			return ca, nil
		}
		var ok bool
		if ca.Edit, ok = edit.(*protocol.WorkspaceEdit); !ok {
			return nil, fmt.Errorf("unable to resolve code action %q", ca.Title)
		}
	}
	return ca, nil
}

// codeActionsMatchingDiagnostics creates code actions for the
// provided diagnostics, by unmarshalling actions bundled in the
// protocol.Diagnostic.Data field or, if there were none, by creating
// actions from edits associated with a matching Diagnostic from the
// set of stored diagnostics for this file.
func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) {
	var actions []protocol.CodeAction
	var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field
	for _, pd := range pds {
		bundled, err := cache.BundledLazyFixes(pd)
		if err != nil {
			return nil, err
		}
		if len(bundled) > 0 {
			for _, fix := range bundled {
				if enabled(fix.Kind) {
					actions = append(actions, fix)
				}
			}
		} else {
			// No bundled actions: keep searching for a match.
			unbundled = append(unbundled, pd)
		}
	}

	for _, pd := range unbundled {
		for _, sd := range s.findMatchingDiagnostics(uri, pd) {
			diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, enabled)
			if err != nil {
				return nil, err
			}
			actions = append(actions, diagActions...)
		}
	}
	return actions, nil
}

func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, enabled func(protocol.CodeActionKind) bool) ([]protocol.CodeAction, error) {
	var actions []protocol.CodeAction
	for _, fix := range sd.SuggestedFixes {
		if !enabled(fix.ActionKind) {
			continue
		}
		var changes []protocol.DocumentChange
		for uri, edits := range fix.Edits {
			fh, err := snapshot.ReadFile(ctx, uri)
			if err != nil {
				return nil, err
			}
			change := protocol.DocumentChangeEdit(fh, edits)
			changes = append(changes, change)
		}
		actions = append(actions, protocol.CodeAction{
			Title:       fix.Title,
			Kind:        fix.ActionKind,
			Edit:        protocol.NewWorkspaceEdit(changes...),
			Command:     fix.Command,
			Diagnostics: []protocol.Diagnostic{*pd},
		})
	}
	return actions, nil
}

func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.Diagnostic) []*cache.Diagnostic {
	s.diagnosticsMu.Lock()
	defer s.diagnosticsMu.Unlock()

	var sds []*cache.Diagnostic
	if fileDiags := s.diagnostics[uri]; fileDiags != nil {
		for _, viewDiags := range fileDiags.byView {
			for _, sd := range viewDiags.diagnostics {
				// extra space may have been trimmed when
				// converting to protocol.Diagnostic
				sameDiagnostic := pd.MessageString() == strings.TrimSpace(sd.Message) &&
					protocol.CompareRange(pd.Range, sd.Range) == 0 &&
					pd.Source == string(sd.Source)

				if sameDiagnostic {
					sds = append(sds, sd)
				}
			}
		}
	}
	return sds
}

func (s *server) getSupportedCodeActions() []protocol.CodeActionKind {
	allCodeActionKinds := make(map[protocol.CodeActionKind]struct{})
	for _, kinds := range s.Options().SupportedCodeActions {
		for kind := range kinds {
			allCodeActionKinds[kind] = struct{}{}
		}
	}
	var result []protocol.CodeActionKind
	for kind := range allCodeActionKinds {
		result = append(result, kind)
	}
	slices.Sort(result)
	return result
}

type unit = struct{}
