// 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 golang

import (
	"context"
	"encoding/json"
	"fmt"
	"go/token"
	"slices"
	"strings"
	"unicode"

	"golang.org/x/mod/module"
	"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/gopls/internal/util/morestrings"
)

// Ths file contains the code to mediate user dialogs in the client

// The logical flow is as follows:
// 1. Client sends 'textDocument/codeAction'
//    gopls respond with one or more CodeActions, including one with kind either
//    'refactor.rewrite.addTags' or 'refactor.rewrite.removeTags'
// 2. Client sends 'codeAction/resolve' with the selected CodeActions
//    gopls returns the same CodeAction
// 3. Client sends 'workspace/executeCommand' to get the dialog form
//    gopls responds with the form to display to the user
// 4. Client sends 'command/resolve' with the filled out form
//    gopls responds, with 'workspace/applyEdit' saying what to do
// 5. Client return ApplyWorkspaceEditResult with applied = true
// 6. Client sends a textDocument/didChange notification, with the edits applied (optional)

// In vscode-go, because it is a standard LSP client, cannot do step 4 as
// described. Instead it calls "workspace/executeCommand" with command
// 'gopls.lsp' and parameter.Method "command/resolve".
//
// ExecuteCommand() calls command.Dispatch() which calls LSP()
// which calls protocol.ServerDispatchCall("command/resolve")
// which calls ResolveCommand. command.Dispatch() switches on param.Command and LSP() passes param.Method
// to ServerDispatchCall

// The neovim flow is simpler, as 'command/resolve' can be invoked directly.

var addTagsForm = []protocol.FormField{
	{
		ID:          "tags",
		Description: `comma-separated list of tags to add; e.g.. "json,xml"`,
		Type:        protocol.FormFieldTypeString{Kind: protocol.FormFieldKindString},
		Required:    true,
		Default:     "json",
	},
	{
		ID:          "transform",
		Description: `transform rule for added tags, e.g., "camelcase' or 'snakecase"`,
		Type: protocol.FormFieldTypeEnum{
			Kind: protocol.FormFieldKindEnum,
			Entries: []protocol.FormEnumEntry{
				{
					Value:       "camelcase",
					Description: "camelCase",
				},
				{
					Value:       "lispcase",
					Description: "lisp-case",
				},
				{
					Value:       "pascalcase",
					Description: "PascalCase",
				},
				{
					Value:       "titlecase",
					Description: "Title Case",
				},
				{
					Value:       "snakecase",
					Description: "snake_case",
				},
			},
		},
		Required: true,
		Default:  "camelcase",
	},
}

var removeTagsForm = []protocol.FormField{
	{
		ID:          "tags",
		Description: `comma-separated list of tags to remove; e.g., "json,xml"`,
		Type:        protocol.FormFieldTypeString{Kind: protocol.FormFieldKindString},
		Required:    true,
		Default:     "json", // TODO(?): put the existing tags here?
	},
}

// ResolveCommand implements the interactive resolution step for workspace commands.
// It inspects the command name within the provided [protocol.ExecuteCommandParams]
// and delegates to the appropriate command-specific resolver (e.g., modify_tags,
// implement_interface).
//
// For full details on the interactive protocol, the multi-step handshake, and the
// conditions under which the parameter is modified or returned as-is, see the
// official documentation on the protocol interface and [protocol.Server.ResolveCommand].
func ResolveCommand(ctx context.Context, params *protocol.ExecuteCommandParams, options settings.ClientOptions) (*protocol.ExecuteCommandParams, error) {
	switch params.Command {
	case "gopls.modify_tags":
		if err := resolveModifyTags(options, params); err != nil {
			return nil, err
		}
	case "gopls.implement_interface":
		if err := resolveImplementInterface(options, params); err != nil {
			return nil, err
		}
	case "gopls.move_declaration":
		if err := resolveMoveDeclaration(options, params); err != nil {
			return nil, err
		}
	}
	return params, nil
}

func resolveModifyTags(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error {
	var a0 command.ModifyTagsArgs
	if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil {
		return err
	}
	switch a0.Modification {
	case "add":
		if !supportsDialog(options, addTagsForm) {
			return nil
		}

		// First call, return the form.
		if len(param.FormAnswers) == 0 {
			param.FormFields = addTagsForm
			return nil
		}

		v0, err := FormAnswer[string](&param.InteractiveParams, "tags")
		if err != nil {
			return err
		}
		if _, err = SanitizeTags(v0); err != nil {
			form := slices.Clone(addTagsForm)
			form[0].Error = err.Error()
			param.FormFields = form
			return nil
		}

		_, err = FormAnswer[string](&param.InteractiveParams, "transform")
		if err != nil {
			return err
		}
		// PJW: what happens when the user enters a bad value? (i think the client handles it)

		param.FormFields = nil
		return nil
	case "remove":
		if !supportsDialog(options, removeTagsForm) {
			return nil
		}

		// First call, return the form
		if len(param.FormAnswers) == 0 {
			// TODO? show the user the current list of tags?
			param.FormFields = removeTagsForm
			return nil
		}

		v, err := FormAnswer[string](&param.InteractiveParams, "tags")
		if err != nil {
			return err
		}
		if _, err := SanitizeTags(v); err != nil {
			form := slices.Clone(addTagsForm)
			form[0].Error = err.Error()
			param.FormFields = form
			return nil
		}

		param.FormFields = nil
		return nil
	default:
		return fmt.Errorf("unsupported modify tags operation: %s", a0.Modification)
	}
}

func mustMarshal(x any) json.RawMessage {
	data, err := json.Marshal(x)
	if err != nil {
		panic(err)
	}
	return json.RawMessage(data)
}

var implementInterfaceFormLazyEnum = []protocol.FormField{
	{
		ID:          "interface",
		Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`,
		Type: protocol.FormFieldTypeLazyEnum{
			Kind:   protocol.FormFieldKindLazyEnum,
			Source: "workspaceSymbol",
			Config: mustMarshal(InteractiveWorkspaceSymbolEnumConfig{
				Kinds: []protocol.SymbolKind{protocol.Interface},
			}),
		},
		Required: true,
		Default:  "error",
	},
}

var implementInterfaceFormString = []protocol.FormField{
	{
		ID:          "interface",
		Description: `fully qualified interface identifier path/to/pkg.interface; e.g., "net.Error"`,
		Type: protocol.FormFieldTypeString{
			Kind: protocol.FormFieldKindString,
		},
		Required: true,
		Default:  "error",
	},
}

var moveDeclarationFormString = []protocol.FormField{
	{
		ID:          "string",
		Description: "destination file uri for the moved declaration, e.g. file:///path/to/file.go",
		Type: protocol.FormFieldTypeFile{
			Kind: "string",
		},
		Required: true,
	},
}

var moveDeclarationFormFile = []protocol.FormField{
	{
		ID:          "file",
		Description: "destination file for the moved declaration",
		Type: protocol.FormFieldTypeFile{
			Kind: "file",
		},
		Required: true,
	},
}

func resolveImplementInterface(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error {
	var a0 command.ImplementInterfaceArgs
	if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil {
		return err
	}

	var form []protocol.FormField
	if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeLazyEnum]; ok {
		form = implementInterfaceFormLazyEnum
	} else if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeString]; ok {
		form = implementInterfaceFormString
	} else {
		// This should not happen, as the gopls should not offer such code
		// action if the language client does not support any kind above.
		return fmt.Errorf("internal error: unsupported interactive input types: %v", options.SupportedInteractiveInputTypes)
	}

	// First call, return the empty form.
	if len(param.FormAnswers) == 0 {
		param.FormFields = form
		return nil
	}

	v, err := FormAnswer[string](&param.InteractiveParams, "interface")
	if err != nil {
		return err
	}

	// Gopls only validates the syntax of the string; it does not verify that
	// the package or interface actually exists in the workspace.
	validInterface := func(ifaceStr string) error {
		if ifaceStr == "error" {
			return nil
		}
		pkgPath, ifaceName, ok := morestrings.CutLast(ifaceStr, ".")
		if !ok {
			return fmt.Errorf(`invalid interface type name: want string of form "example.com/pkg.Type", got %q`, ifaceStr)
		}

		if err := module.CheckImportPath(pkgPath); err != nil {
			return fmt.Errorf("invalid package path %w", err)
		}

		if !token.IsIdentifier(ifaceName) {
			return fmt.Errorf("invalid type name: %q", ifaceName)
		}

		return nil
	}

	if err := validInterface(v); err != nil {
		// The client only sends back answers, not the original form fields.
		// Clone the static form template so we can attach the validation
		// error and send the complete form back for the client to re-render.
		form := slices.Clone(form)
		form[0].Error = err.Error()
		param.FormFields = form
		return nil
	}

	param.FormFields = nil
	return nil
}

func resolveMoveDeclaration(options settings.ClientOptions, param *protocol.ExecuteCommandParams) error {
	var a0 command.MoveDeclarationArgs
	if err := command.UnmarshalArgs(param.Arguments, &a0); err != nil {
		return err
	}
	var form []protocol.FormField
	if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeFile]; ok {
		form = moveDeclarationFormFile
	} else if ok := options.SupportedInteractiveInputTypes[settings.InteractiveInputTypeString]; ok {
		form = moveDeclarationFormString
	} else {
		// This should not happen because gopls should not offer this code action if the
		// language client does not support any kind above.
		return fmt.Errorf("internal error: unsupported interactive input types: %v", options.SupportedInteractiveInputTypes)
	}

	// First call, return the empty form.
	if len(param.FormAnswers) == 0 {
		param.FormFields = form
		return nil
	}

	file, err := FormAnswer[string](&param.InteractiveParams, "file")
	if err != nil {
		return err
	}
	if _, err := protocol.ParseDocumentURI(file); err != nil {
		return err
	}
	param.FormFields = nil
	return nil
}

// FormAnswer finds, validates, and returns the unique answer for id.
//
// It uses a linear scan since the number of answers is small (usually < 5).
func FormAnswer[T any](params *protocol.InteractiveParams, id string) (v T, err error) {
	matches := 0
	for _, ans := range params.FormAnswers {
		if ans.ID == id {
			matches++
			val, ok := ans.Value.(T)
			if !ok {
				return v, fmt.Errorf("form answer %q has unexpected type %T, want %T", id, ans.Value, v)
			}
			v = val
		}
	}
	if matches == 0 {
		return v, fmt.Errorf("form lacks answer %q", id)
	} else if matches > 1 {
		return v, fmt.Errorf("form contains duplicate answer %q", id)
	}
	return v, nil
}

// SanitizeTags cleans up comma-separated tags and ensures they are valid.
func SanitizeTags(tags string) (string, error) {
	parts := strings.Split(tags, ",")
	var clean []string

	for _, p := range parts {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}

		// Use strings.ContainsFunc instead of a manual byte loop.
		// It returns true if any rune in the string matches the condition.
		if strings.ContainsFunc(p, func(r rune) bool {
			// Space, colon, quote, or any non-printable character (like control chars)
			return r == ' ' || r == ':' || r == '"' || !unicode.IsPrint(r)
		}) {
			return "", fmt.Errorf("illegal tag %q: cannot contain spaces, quotes, colons, or control characters", p)
		}

		clean = append(clean, p)
	}

	return strings.Join(clean, ","), nil
}
