package ask

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os/exec"
	"regexp"
	"strings"

	"github.com/MakeNowJust/heredoc/v2"
	"github.com/google/shlex"
	"github.com/spf13/cobra"

	"gitlab.com/gitlab-org/cli/internal/api"
	"gitlab.com/gitlab-org/cli/internal/cmdutils"
	"gitlab.com/gitlab-org/cli/internal/glrepo"
	"gitlab.com/gitlab-org/cli/internal/iostreams"
	"gitlab.com/gitlab-org/cli/internal/mcpannotations"
	"gitlab.com/gitlab-org/cli/internal/run"
)

type request struct {
	Prompt string `json:"prompt"`
	Model  string `json:"model"`
}

type response struct {
	Predictions []struct {
		Candidates []struct {
			Content string `json:"content"`
		} `json:"candidates"`
	} `json:"predictions"`
}

type result struct {
	Commands    []string `json:"commands"`
	Explanation string   `json:"explanation"`
}

type opts struct {
	Prompt    string
	IO        *iostreams.IOStreams
	apiClient func(repoHost string) (*api.Client, error)
	Git       bool
	BaseRepo  func() (glrepo.Interface, error)
}

var (
	cmdHighlightRegexp = regexp.MustCompile("`+\n?([^`]*)\n?`+\n?")
	cmdExecRegexp      = regexp.MustCompile("```([^`]*)```")
	vertexAI           = "vertexai"
)

const (
	runCmdsQuestion   = "Would you like to run these Git commands?"
	gitCmd            = "git"
	gitCmdAPIPath     = "ai/llm/git_command"
	spinnerText       = "Generating Git commands..."
	aiResponseErr     = "Error: AI response has not been generated correctly."
	apiUnreachableErr = "Error: API is unreachable."
)

func NewCmdAsk(f cmdutils.Factory) *cobra.Command {
	opts := &opts{
		IO:        f.IO(),
		apiClient: f.ApiClient,
		BaseRepo:  f.BaseRepo,
	}

	duoAskCmd := &cobra.Command{
		Use:   "ask <prompt>",
		Short: "Generate Git commands from natural language.",
		Long: heredoc.Doc(`
			Generate Git commands from natural language using AI assistance.

			Describe what you want to do in plain language, and GitLab Duo suggests the
			appropriate Git commands. The AI provides both the commands and an explanation.

			After you receive the suggested commands, you can execute them directly from
			the CLI. You must confirm each command before it runs.

			Use this command to:

			- Retrieve forgotten Git commands.
			- Learn how to accomplish specific Git operations.
			- Get guidance on complex Git workflows.
			- Reduce context switching during development.
		`),
		Example: heredoc.Doc(`
			$ glab duo ask list last 10 commit titles
			> A list of Git commands to show the titles of the latest 10 commits with an explanation and an option to execute the commands.

			$ glab duo ask how do I undo my last commit
			> Suggestions for undoing the last commit with explanations of different approaches.

			$ glab duo ask show me files changed in the last commit
			> Commands to display files modified in the most recent commit.
		`),
		Annotations: map[string]string{
			mcpannotations.Destructive: "true",
		},
		RunE: func(cmd *cobra.Command, args []string) error {
			if !opts.Git {
				return nil
			}

			if len(args) == 0 {
				return nil
			}

			opts.Prompt = strings.Join(args, " ")

			result, err := opts.Result()
			if err != nil {
				return err
			}

			opts.displayResult(result)

			if len(result.Commands) > 0 {
				if err := opts.executeCommands(cmd.Context(), result.Commands); err != nil {
					return err
				}
			}
			return nil
		},
	}

	duoAskCmd.Flags().BoolVarP(&opts.Git, "git", "", true, "Ask a question about Git.")

	return duoAskCmd
}

func (opts *opts) Result() (*result, error) {
	opts.IO.StartSpinner(spinnerText)
	defer opts.IO.StopSpinner("")

	var repoHost string
	if baseRepo, err := opts.BaseRepo(); err == nil {
		repoHost = baseRepo.RepoHost()
	}

	c, err := opts.apiClient(repoHost)
	if err != nil {
		return nil, cmdutils.WrapError(err, "failed to get HTTP client.")
	}
	client := c.Lab()

	body := request{Prompt: opts.Prompt, Model: vertexAI}
	request, err := client.NewRequest(http.MethodPost, gitCmdAPIPath, body, nil)
	if err != nil {
		return nil, cmdutils.WrapError(err, "failed to create a request.")
	}

	var r response
	_, err = client.Do(request, &r)
	if err != nil {
		return nil, cmdutils.WrapError(err, apiUnreachableErr)
	}

	if len(r.Predictions) == 0 || len(r.Predictions[0].Candidates) == 0 {
		return nil, errors.New(aiResponseErr)
	}

	content := r.Predictions[0].Candidates[0].Content

	var cmds []string
	for _, cmd := range cmdExecRegexp.FindAllString(content, -1) {
		cmds = append(cmds, strings.Trim(cmd, "\n`"))
	}

	return &result{
		Commands:    cmds,
		Explanation: content,
	}, nil
}

func (opts *opts) displayResult(result *result) {
	color := opts.IO.Color()

	opts.IO.LogInfo(color.Bold("Commands:\n"))

	for _, cmd := range result.Commands {
		opts.IO.LogInfo(color.Green(cmd))
	}

	opts.IO.LogInfo(color.Bold("\nExplanation:\n"))
	explanation := cmdHighlightRegexp.ReplaceAllString(result.Explanation, color.Green("$1"))
	opts.IO.LogInfo(explanation + "\n")
}

func (opts *opts) executeCommands(ctx context.Context, commands []string) error {
	color := opts.IO.Color()

	confirmed := true
	question := color.Bold(runCmdsQuestion)
	if err := opts.IO.Confirm(ctx, &confirmed, question); err != nil {
		return err
	}

	if !confirmed {
		return nil
	}

	for _, command := range commands {
		if err := opts.executeCommand(ctx, command); err != nil {
			return err
		}
	}

	return nil
}

func (opts *opts) executeCommand(ctx context.Context, cmd string) error {
	gitArgs, err := shlex.Split(cmd)
	if err != nil {
		return nil
	}

	if gitArgs[0] != gitCmd {
		return nil
	}

	color := opts.IO.Color()
	question := fmt.Sprintf("Run `%s`", color.Green(cmd))
	confirmed := true
	if err := opts.IO.Confirm(ctx, &confirmed, question); err != nil {
		return err
	}

	if !confirmed {
		return nil
	}

	execCmd := exec.Command("git", gitArgs[1:]...)
	output, err := run.PrepareCmd(execCmd).Output()
	if err != nil {
		return err
	}

	if len(output) == 0 {
		return nil
	}

	if opts.IO.StartPager() != nil {
		return fmt.Errorf("failed to start pager: %q", err)
	}
	defer opts.IO.StopPager()

	opts.IO.LogInfo(string(output))

	return nil
}
