package cmd

import (
	"bytes"
	"cmp"
	"errors"
	"fmt"
	"html/template"
	"net/http"
	"os"
	"regexp"
	"slices"
	"strings"

	cqapi "github.com/cloudquery/cloudquery-api-go"
	cqauth "github.com/cloudquery/cloudquery-api-go/auth"
	"github.com/cloudquery/cloudquery/cli/v6/internal/analytics"
	"github.com/cloudquery/cloudquery/cli/v6/internal/api"
	"github.com/cloudquery/cloudquery/cli/v6/internal/auth"
	"github.com/fatih/color"
	"github.com/manifoldco/promptui"
	"github.com/samber/lo"
	"github.com/spf13/cobra"
)

const (
	initShort = `Generate a configuration file for a sync`
	initLong  = `Generate a configuration file for a sync

### Modes

The ` + "`init`" + ` command operates in one of three modes depending on your authentication state and flags:

**AI-assisted mode** (default when logged in)

Activates when you are logged in to a team (` + "`cloudquery login`" + `) and don't specify ` + "`--source`" + ` or ` + "`--destination`" + `. Launches an interactive AI chat session that walks you through the setup process — selecting integrations, generating YAML configuration files, testing connections, and giving you some example queries.

Type ` + "`exit`" + ` or ` + "`quit`" + ` to end the conversation. Use ` + "`--resume-conversation`" + ` to continue a previous session instead of starting a new one.

**Basic interactive mode**

Activates when you pass ` + "`--disable-ai`" + `, or as a fallback if the AI assistant is unavailable. Presents a searchable picker to select source and destination integrations, then generates a configuration file from their default templates.

**Non-interactive mode**

Activates when both ` + "`--source`" + ` and ` + "`--destination`" + ` are specified. Generates the configuration file directly without prompts.

Authentication via ` + "`cloudquery login`" + ` is required for AI-assisted and basic interactive modes.`
	initExample = `# Display prompts to select source and destination plugins and generate a configuration file from them
cloudquery init
# Generate a configuration file for a sync from aws to bigquery
cloudquery init --source aws --destination bigquery
# Display a prompt to select a source plugin and generate a configuration file for a sync from it to bigquery
cloudquery init --destination bigquery
# Display a prompt to select a destination plugin and generate a configuration file for a sync from aws to it
cloudquery init --source aws
# Accept all defaults and generate a configuration file for a sync from the first source and destination plugins
cloudquery init --yes`
)

var (
	sourcesOrder      = []string{"aws", "azure", "gcp"}
	destinationsOrder = []string{"postgresql", "bigquery", "s3"}
	bold              = color.New(color.Bold)
	successful        = color.New(color.Bold, color.FgGreen)
	link              = color.New(color.Bold, color.FgCyan)
	errorColor        = color.New(color.Bold, color.FgRed)
)

func newCmdInit() *cobra.Command {
	cmd := &cobra.Command{
		Use:     "init",
		Short:   initShort,
		Long:    initLong,
		Example: initExample,
		Args:    cobra.ExactArgs(0),
		RunE:    initCmd,
	}
	cmd.Flags().String("source", "", "Source plugin name or path")
	cmd.Flags().String("destination", "", "Destination plugin name or path")
	cmd.Flags().String("spec-path", "", "Output spec file path")
	cmd.Flags().Bool("yes", false, "Accept all defaults")
	cmd.Flags().Bool("disable-ai", false, "Disable AI assistant")
	cmd.Flags().Bool("resume-conversation", false, "Resume existing AI conversation instead of starting a new one")
	return cmd
}

func normalizePluginPath(pluginNameOrPath string) (string, error) {
	parts := strings.Split(pluginNameOrPath, "/")
	if len(parts) == 1 {
		return "cloudquery/" + pluginNameOrPath, nil
	}
	if len(parts) != 2 {
		return "", errors.New("invalid plugin path")
	}
	return pluginNameOrPath, nil
}

func parseFlags(cmd *cobra.Command) (source, destination, specPath string, acceptDefaults, disableAI, resumeConversation bool, allErrors error) {
	source, err := cmd.Flags().GetString("source")
	allErrors = errors.Join(allErrors, err)
	if source != "" {
		source, err = normalizePluginPath(source)
		allErrors = errors.Join(allErrors, err)
	}
	destination, err = cmd.Flags().GetString("destination")
	allErrors = errors.Join(allErrors, err)
	if destination != "" {
		destination, err = normalizePluginPath(destination)
		allErrors = errors.Join(allErrors, err)
	}
	specPath, err = cmd.Flags().GetString("spec-path")
	allErrors = errors.Join(allErrors, err)

	acceptDefaults, err = cmd.Flags().GetBool("yes")
	allErrors = errors.Join(allErrors, err)

	disableAI, err = cmd.Flags().GetBool("disable-ai")
	allErrors = errors.Join(allErrors, err)

	resumeConversation, err = cmd.Flags().GetBool("resume-conversation")
	allErrors = errors.Join(allErrors, err)
	return source, destination, specPath, acceptDefaults, disableAI, resumeConversation, allErrors
}

func pluginFilter(pluginPath string, kind cqapi.PluginKind) func(plugin cqapi.ListPlugin) bool {
	return func(plugin cqapi.ListPlugin) bool {
		return plugin.TeamName+"/"+plugin.Name == pluginPath && plugin.Kind == kind && plugin.LatestVersion != nil
	}
}

func pluginName(plugin cqapi.ListPlugin, _ int) string {
	return plugin.Name
}

func officialReleasedPluginsByKind(kind cqapi.PluginKind) func(plugin cqapi.ListPlugin, _ int) bool {
	return func(plugin cqapi.ListPlugin, _ int) bool {
		return plugin.Kind == kind && plugin.Official && plugin.ReleaseStage != cqapi.PluginReleaseStageComingSoon && plugin.LatestVersion != nil
	}
}

func pluginsSorter(prioritySlice []string) func(a, b cqapi.ListPlugin) int {
	return func(a, b cqapi.ListPlugin) int {
		indexOfA := lo.IndexOf(prioritySlice, a.Name)
		indexOfB := lo.IndexOf(prioritySlice, b.Name)
		if indexOfA == -1 && indexOfB != -1 {
			return 1
		}
		if indexOfA != -1 && indexOfB == -1 {
			return -1
		}
		if indexOfA == -1 && indexOfB == -1 {
			return cmp.Compare(a.Name, b.Name)
		}
		return cmp.Compare(indexOfA, indexOfB)
	}
}

func extractYamlFromMarkdownCodeBlock(markdown string) string {
	re := regexp.MustCompile("```yaml.*?\n([\\s\\S]+?)\n```")

	matches := re.FindStringSubmatch(markdown)
	if len(matches) < 2 {
		return ""
	}

	return matches[1]
}

func defaultConfigForPlugin(plugin cqapi.ListPlugin) *strings.Builder {
	tmpl := `kind: {{.Kind}}
spec:
  name: {{.Name}}
  path: {{.TeamName}}/{{.Name}}
  version: {{.LatestVersion}}
`
	var buf bytes.Buffer
	t := template.Must(template.New("config").Parse(tmpl))
	_ = t.Execute(&buf, plugin)

	sb := strings.Builder{}
	sb.WriteString(buf.String())
	return &sb
}

func configForSourcePlugin(source cqapi.ListPlugin, version *cqapi.PluginVersionDetails) string {
	exampleConfig := extractYamlFromMarkdownCodeBlock(version.ExampleConfig)
	if exampleConfig != "" {
		return exampleConfig
	}

	defaultConfig := defaultConfigForPlugin(source)
	defaultConfig.WriteString("  tables: ['*']\n")
	defaultConfig.WriteString("  destinations: ['DESTINATION_NAME']")
	return defaultConfig.String()
}

func configForDestinationPlugin(destination cqapi.ListPlugin, version *cqapi.PluginVersionDetails) string {
	exampleConfig := extractYamlFromMarkdownCodeBlock(version.ExampleConfig)
	if exampleConfig != "" {
		return exampleConfig
	}

	defaultConfig := defaultConfigForPlugin(destination)
	return defaultConfig.String()
}

func selectSource(allPlugins []cqapi.ListPlugin, acceptDefaults bool) (string, error) {
	officialSources := lo.Filter(allPlugins, officialReleasedPluginsByKind(cqapi.PluginKindSource))
	slices.SortStableFunc(officialSources, pluginsSorter(sourcesOrder))
	if acceptDefaults {
		return officialSources[0].Name, nil
	}

	prompt := promptui.Select{
		Label:             "Select Source Plugin",
		Items:             lo.Map(officialSources, pluginName),
		Stdin:             os.Stdin,
		Size:              10,
		StartInSearchMode: true,
		Searcher: func(input string, index int) bool {
			return strings.Contains(officialSources[index].Name, input)
		},
	}

	_, source, err := prompt.Run()
	if err != nil {
		return "", fmt.Errorf("source prompt failed %w", err)
	}

	return source, nil
}

func selectDestination(allPlugins []cqapi.ListPlugin, acceptDefaults bool) (string, error) {
	officialDestinations := lo.Filter(allPlugins, officialReleasedPluginsByKind(cqapi.PluginKindDestination))
	slices.SortStableFunc(officialDestinations, pluginsSorter(destinationsOrder))
	if acceptDefaults {
		return officialDestinations[0].Name, nil
	}

	prompt := promptui.Select{
		Label:             "Select Destination Plugin",
		Items:             lo.Map(officialDestinations, pluginName),
		Stdin:             os.Stdin,
		Size:              10,
		StartInSearchMode: true,
		Searcher: func(input string, index int) bool {
			return strings.Contains(officialDestinations[index].Name, input)
		},
	}

	_, destination, err := prompt.Run()
	if err != nil {
		return "", fmt.Errorf("destination prompt failed %w", err)
	}

	return destination, nil
}

func linkForPlugin(plugin cqapi.ListPlugin) string {
	return link.Sprintf("https://www.cloudquery.io/hub/plugins/%s/%s/%s", plugin.Kind, plugin.TeamName, plugin.Name)
}

func initCmd(cmd *cobra.Command, args []string) (initCommandError error) {
	ctx := cmd.Context()
	source, destination, specPath, acceptDefaults, disableAI, resumeConversation, err := parseFlags(cmd)
	analytics.TrackInitStarted(ctx, invocationUUID.UUID, analytics.InitEvent{
		Source:         source,
		Destination:    destination,
		AcceptDefaults: acceptDefaults,
		SpecPath:       specPath,
		Error:          err,
	})
	defer func() {
		analytics.TrackInitCompleted(ctx, invocationUUID.UUID, analytics.InitEvent{
			Source:         source,
			Destination:    destination,
			AcceptDefaults: acceptDefaults,
			SpecPath:       specPath,
			Error:          initCommandError,
		})
	}()
	if err != nil {
		return err
	}

	authClient := cqauth.NewTokenClient()
	token, err := authClient.GetToken()
	var user *cqapi.User
	if err == nil {
		user, _ = auth.GetUser(cmd.Context(), token)
	}

	team, _ := auth.GetTeamForToken(cmd.Context(), token)

	apiClient, err := api.NewAnonymousClient()
	var apiClientWithoutRetries *cqapi.ClientWithResponses
	if err != nil {
		return err
	}
	if user != nil {
		apiClient, err = api.NewClient(token.Value)
		if err != nil {
			return err
		}

		apiClientWithoutRetries, err = api.NewClient(token.Value, cqapi.WithHTTPClient(http.DefaultClient))
		if err != nil {
			return err
		}
	}

	// Check if user and team are set, and if so, run AI command
	if user != nil && team != "" && !disableAI && source == "" && destination == "" {
		err := api.NewConversation(ctx, apiClientWithoutRetries, team, resumeConversation)
		if err != nil && err != api.ErrDisabled {
			return err
		}
		if err != api.ErrDisabled {
			// User and team are set, endpoint is not FF disabled, proceed to run the AI command
			err := aiCmd(ctx, apiClient, team, resumeConversation)

			// This is unintuitive:
			// - if AI works out, we're done
			// - if AI fails, we output an obfuscated error and fallback to basic interactive mode
			if err == nil {
				return nil
			}
			errorColor.Println("There was an issue with the AI assistant. Falling back to basic interactive mode...")
			fmt.Println()
		}
	} else if (user == nil || team == "") && source == "" && destination == "" && !disableAI {
		return errors.New("authentication required for interactive mode. Please run `cloudquery login` first, or supply source and destination plugins, or else use the --disable-ai flag to run basic interactive mode")
	}

	fmt.Println("Fetching plugins...")
	allPlugins, err := api.ListAllPlugins(apiClient)
	if err != nil {
		return err
	}

	var notFoundPluginsErrors error
	if source != "" {
		sourcePluginFilter := pluginFilter(source, cqapi.PluginKindSource)
		sourceFound := lo.SomeBy(allPlugins, sourcePluginFilter)
		if !sourceFound {
			notFoundPluginsErrors = errors.Join(notFoundPluginsErrors, fmt.Errorf("source plugin %q not found", source))
		}
	}
	if destination != "" {
		destinationPluginFilter := pluginFilter(destination, cqapi.PluginKindDestination)
		destinationFound := lo.SomeBy(allPlugins, destinationPluginFilter)
		if !destinationFound {
			notFoundPluginsErrors = errors.Join(notFoundPluginsErrors, fmt.Errorf("destination plugin %q not found", destination))
		}
	}

	if notFoundPluginsErrors != nil {
		return notFoundPluginsErrors
	}

	if source == "" {
		source, err = selectSource(allPlugins, acceptDefaults)
		if err != nil {
			return err
		}
		source, _ = normalizePluginPath(source)
	}
	_, sourceIndex, _ := lo.FindIndexOf(allPlugins, pluginFilter(source, cqapi.PluginKindSource))

	if destination == "" {
		destination, err = selectDestination(allPlugins, acceptDefaults)
		if err != nil {
			return err
		}
		destination, _ = normalizePluginPath(destination)
	}
	_, destinationIndex, _ := lo.FindIndexOf(allPlugins, pluginFilter(destination, cqapi.PluginKindDestination))

	sourcePlugin := allPlugins[sourceIndex]
	fmt.Printf("Getting configuration for source plugin %s...\n", bold.Sprintf("%s/%s@%s", sourcePlugin.TeamName, sourcePlugin.Name, *sourcePlugin.LatestVersion))
	sourceVersion, err := api.GetPluginVersion(apiClient, sourcePlugin.TeamName, sourcePlugin.Kind, sourcePlugin.Name, *sourcePlugin.LatestVersion)
	if err != nil {
		return fmt.Errorf("failed to get source plugin %s/%s@%s version %w", sourcePlugin.TeamName, sourcePlugin.Name, *sourcePlugin.LatestVersion, err)
	}

	destinationPlugin := allPlugins[destinationIndex]
	fmt.Printf("Getting configuration for destination plugin %s...\n", bold.Sprintf("%s/%s@%s", destinationPlugin.TeamName, destinationPlugin.Name, *destinationPlugin.LatestVersion))
	destinationVersion, err := api.GetPluginVersion(apiClient, destinationPlugin.TeamName, destinationPlugin.Kind, destinationPlugin.Name, *destinationPlugin.LatestVersion)
	if err != nil {
		return fmt.Errorf("failed to get destination plugin %s/%s@%s version %w", destinationPlugin.TeamName, destinationPlugin.Name, *destinationPlugin.LatestVersion, err)
	}

	if specPath == "" {
		specPath = sourcePlugin.Name + "_to_" + destinationPlugin.Name + ".yaml"
	}
	fmt.Printf("Writing spec to %s...\n", bold.Sprint(specPath))
	var yamlSpec strings.Builder
	sourceConfig := configForSourcePlugin(sourcePlugin, sourceVersion)
	yamlSpec.WriteString(strings.ReplaceAll(sourceConfig, "DESTINATION_NAME", destinationPlugin.Name))
	yamlSpec.WriteString("\n---\n")
	yamlSpec.WriteString(configForDestinationPlugin(destinationPlugin, destinationVersion))

	if err := os.WriteFile(specPath, []byte(yamlSpec.String()), 0644); err != nil {
		return fmt.Errorf("failed to write spec file %w", err)
	}

	if user != nil {
		successful.Println("Sync spec file generated successfully!")
		fmt.Println()
		fmt.Println("Next steps:")
		fmt.Printf("1. Review the generated config file %s and make sure to fill in all authentication details. Learn more about the plugins configuration at:\n", bold.Sprint(specPath))
		fmt.Printf("   %s: %s\n", bold.Sprint(sourcePlugin.DisplayName), linkForPlugin(sourcePlugin))
		fmt.Printf("   %s: %s\n", bold.Sprint(destinationPlugin.DisplayName), linkForPlugin(destinationPlugin))
		fmt.Println("2. Run the following command to start the sync:")
		bold.Printf("cloudquery sync %s\n", specPath)
	} else {
		successful.Println("Sync spec file generated successfully!")
		fmt.Println()
		fmt.Println("Next steps:")
		fmt.Printf("1. Review the generated config file %s and make sure to fill in all authentication details. Learn more about the plugins configuration at:\n", bold.Sprint(specPath))
		fmt.Printf("   %s: %s\n", bold.Sprint(sourcePlugin.DisplayName), linkForPlugin(sourcePlugin))
		fmt.Printf("   %s: %s\n", bold.Sprint(destinationPlugin.DisplayName), linkForPlugin(destinationPlugin))
		fmt.Println("2. Run the following command to log in:")
		bold.Printf("cloudquery login\n")
		fmt.Println()
		fmt.Println("3. Run the following command to start the sync:")
		bold.Printf("cloudquery sync %s\n", specPath)
	}
	return nil
}
