package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/manifoldco/promptui"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	"gopkg.in/yaml.v3"

	"github.com/Azure/draft/pkg/config"
	dryrunpkg "github.com/Azure/draft/pkg/dryrun"
	"github.com/Azure/draft/pkg/filematches"
	"github.com/Azure/draft/pkg/handlers"
	"github.com/Azure/draft/pkg/linguist"
	"github.com/Azure/draft/pkg/prompts"
	"github.com/Azure/draft/pkg/reporeader"
	"github.com/Azure/draft/pkg/reporeader/readers"
	"github.com/Azure/draft/pkg/templatewriter"
	"github.com/Azure/draft/pkg/templatewriter/writers"
	"github.com/Azure/draft/template"
)

// ErrNoLanguageDetected is raised when `draft create` does not detect source
// code for linguist to classify, or if there are no packs available for the detected languages.
var ErrNoLanguageDetected = errors.New("no supported languages were detected")
var flagVariablesMap = make(map[string]string)

const LANGUAGE_VARIABLE = "LANGUAGE"
const TWO_SPACES = "  "

// Flag defaults
const emptyDefaultFlagValue = ""
const currentDirDefaultFlagValue = "."

const DOCKERFILES_DIR = "dockerfiles"

func listSupportedLanguages() ([]string, error) {
	var supportedLanguages []string
	entries, err := template.Templates.ReadDir(DOCKERFILES_DIR)
	if err != nil {
		return supportedLanguages, fmt.Errorf("reading supported languages from embedded fs: %w", err)
	}
	for _, d := range entries {
		if d.IsDir() {
			supportedLanguages = append(supportedLanguages, d.Name())
		}
	}
	return supportedLanguages, nil
}

type createCmd struct {
	lang       string
	dest       string
	deployType string

	dockerfileOnly    bool
	deploymentOnly    bool
	skipFileDetection bool
	flagVariables     []string

	createConfigPath string
	createConfig     *CreateConfig

	templateWriter           templatewriter.TemplateWriter
	templateVariableRecorder config.TemplateVariableRecorder
	repoReader               reporeader.RepoReader
}

func newCreateCmd() *cobra.Command {
	cc := &createCmd{}

	cmd := &cobra.Command{
		Use:   "create [flags]",
		Short: "Add minimum required files to the directory",
		Long:  "This command will add the minimum required files to the local directory for your Kubernetes deployment.",
		RunE: func(cmd *cobra.Command, args []string) error {
			if err := cc.initConfig(); err != nil {
				return err
			}
			return cc.run()
		},
	}

	f := cmd.Flags()

	f.StringVarP(&cc.createConfigPath, "create-config", "c", emptyDefaultFlagValue, "specify the path to the configuration file")
	f.StringVarP(&cc.lang, "language", "l", emptyDefaultFlagValue, "specify the language used to create the Kubernetes deployment")
	f.StringVarP(&cc.dest, "destination", "d", currentDirDefaultFlagValue, "specify the path to the project directory")
	f.StringVarP(&cc.deployType, "deploy-type", "", emptyDefaultFlagValue, "specify deployment type (eg. helm, kustomize, manifests)")
	f.BoolVar(&cc.dockerfileOnly, "dockerfile-only", false, "only create Dockerfile in the project directory")
	f.BoolVar(&cc.deploymentOnly, "deployment-only", false, "only create deployment files in the project directory")
	f.BoolVar(&cc.skipFileDetection, "skip-file-detection", false, "skip file detection step")
	f.StringArrayVarP(&cc.flagVariables, "variable", "", []string{}, "pass template variables (e.g. --variable PORT=8080 --variable APPNAME=test)")

	return cmd
}

func (cc *createCmd) initConfig() error {
	if cc.createConfigPath != "" {
		log.Debug("loading config")
		configBytes, err := os.ReadFile(cc.createConfigPath)
		if err != nil {
			return err
		}

		var cfg CreateConfig
		if err = yaml.Unmarshal(configBytes, &cfg); err != nil {
			return err
		}
		cc.createConfig = &cfg
		return nil
	}

	//TODO: create a config for the user and save it for subsequent uses
	cc.createConfig = &CreateConfig{}

	return nil
}

func (cc *createCmd) run() error {
	log.Debugf("config: %s", cc.createConfigPath)
	log.Debugf("interactive: %t", interactive)

	flagVariablesMap = flagVariablesToMap(cc.flagVariables)

	var dryRunRecorder *dryrunpkg.DryRunRecorder
	if dryRun {
		dryRunRecorder = dryrunpkg.NewDryRunRecorder()
		cc.templateVariableRecorder = dryRunRecorder
		cc.templateWriter = dryRunRecorder
	} else {
		cc.templateWriter = &writers.LocalFSWriter{}
	}
	cc.repoReader = &readers.LocalFSReader{}

	detectedLangDraftConfig, languageName, err := cc.detectLanguage()
	if err != nil {
		return err
	}

	err = cc.createFiles(detectedLangDraftConfig, languageName)
	if dryRun {
		cc.templateVariableRecorder.Record(LANGUAGE_VARIABLE, languageName)
		dryRunText, err := json.MarshalIndent(dryRunRecorder.DryRunInfo, "", TWO_SPACES)
		if err != nil {
			return err
		}
		fmt.Println(string(dryRunText))
		if dryRunFile != "" {
			log.Printf("writing dry run info to file %s", dryRunFile)
			err = os.WriteFile(dryRunFile, dryRunText, 0644)
			if err != nil {
				return err
			}
		}
	}
	return err
}

// detectLanguage detects the language used in a project destination directory
// It returns the DraftConfig for that language and the name of the language
func (cc *createCmd) detectLanguage() (*handlers.Template, string, error) {
	hasGo := false
	hasGoMod := false
	var langs []*linguist.Language
	var err error
	supportedLanguages, err := listSupportedLanguages()
	if err != nil {
		log.Errorf("loading supported languages: %s", err.Error())
	}
	log.Debugf("loaded supported languages: %v", supportedLanguages)
	if cc.createConfig.LanguageType == "" {
		if cc.lang != "" {
			cc.createConfig.LanguageType = cc.lang
		} else {
			log.Info("--- Detecting Language ---")
			langs, err = linguist.ProcessDir(cc.dest)
			log.Debugf("linguist.ProcessDir(%v) result:\n\nError: %v", cc.dest, err)
			if err != nil {
				return nil, "", fmt.Errorf("there was an error detecting the language: %s", err)
			}
			if len(langs) == 0 {
				if !interactive {
					return nil, "", ErrNoLanguageDetected
				}
				langs, err = promptLanguageSelection(supportedLanguages)
				if err != nil {
					return nil, "", fmt.Errorf("prompting for language: %w", err)
				}
			}
			for _, lang := range langs {
				log.Debugf("%s:\t%f (%s)", lang.Language, lang.Percent, lang.Color)
				// For now let's check here for weird stuff like go module support
				hasGoMod = true
				if interactive && lang.Language == "Go" {
					hasGo = true

					selection := &promptui.Select{
						Label: "Linguist detected Go, do you use Go Modules?",
						Items: []string{"yes", "no"},
					}

					_, selectResponse, err := selection.Run()
					if err != nil {
						return nil, "", err
					}
					hasGoMod = strings.EqualFold(selectResponse, "yes")

				}

				if interactive && lang.Language == "Java" {

					selection := &promptui.Select{
						Label: "Linguist detected Java, are you using maven or gradle?",
						Items: []string{"maven", "gradle", "gradlew"},
					}

					_, selectResponse, err := selection.Run()
					if err != nil {
						return nil, "", err
					}

					if selectResponse == "gradle" {
						lang.Language = "Gradle"
					} else if selectResponse == "gradlew" {
						lang.Language = "Gradlew"
					}
				}
			}

			log.Debugf("detected %d langs", len(langs))

			if len(langs) == 0 {
				return nil, "", ErrNoLanguageDetected
			}
		}
	}

	if cc.createConfig.LanguageType != "" {
		log.Debug("using configuration language")
		lowerLang := strings.ToLower(cc.createConfig.LanguageType)
		langDockerfileTemplate, err := handlers.GetTemplate(fmt.Sprintf("dockerfile-%s", lowerLang), "", cc.dest, cc.templateWriter)
		if err != nil {
			return nil, "", err
		}
		if langDockerfileTemplate == nil {
			return nil, "", fmt.Errorf("could not find a template for %s", cc.createConfig.LanguageType)
		}

		return langDockerfileTemplate, lowerLang, nil
	}

	for _, lang := range langs {
		detectedLang := linguist.Alias(lang)
		log.Infof("--> Draft detected %s (%f%%)\n", detectedLang.Language, detectedLang.Percent)
		lowerLang := strings.ToLower(detectedLang.Language)
		if handlers.IsValidTemplate(fmt.Sprintf("dockerfile-%s", lowerLang)) {
			if lowerLang == "go" && hasGo && hasGoMod {
				log.Debug("detected go and go module")
				lowerLang = "gomodule"
			}
			langDockerfileTemplate, err := handlers.GetTemplate(fmt.Sprintf("dockerfile-%s", lowerLang), "", cc.dest, cc.templateWriter)
			if err != nil {
				return nil, "", err
			}
			if langDockerfileTemplate == nil {
				return nil, "", fmt.Errorf("could not find a template for detected language %s", detectedLang.Language)
			}
			return langDockerfileTemplate, lowerLang, nil
		}
		log.Infof("--> Could not find a pack for %s. Trying to find the next likely language match...", detectedLang.Language)
	}
	return nil, "", ErrNoLanguageDetected
}

func (cc *createCmd) generateDockerfile(dockerfileTemplate *handlers.Template, lowerLang string) error {
	log.Info("--- Dockerfile Creation ---")

	// Extract language-specific defaults from repo
	extractedValues, err := dockerfileTemplate.ExtractDefaults(lowerLang, cc.repoReader)
	if err != nil {
		return err
	}

	// Check for existing duplicate defaults
	for k, v := range extractedValues {
		variableExists := false
		for i, variable := range dockerfileTemplate.Config.Variables {
			if k == variable.Name {
				variableExists = true
				dockerfileTemplate.Config.Variables[i].Default.Value = v
				break
			}
		}
		if !variableExists {
			dockerfileTemplate.Config.Variables = append(dockerfileTemplate.Config.Variables, &config.BuilderVar{
				Name: k,
				Default: config.BuilderVarDefault{
					Value: v,
				},
			})
		}
	}

	if cc.createConfig.LanguageVariables != nil || !interactive {
		dockerfileTemplate.Config.ApplyDefaultVariables()
		err = validateConfigInputsToPrompts(dockerfileTemplate.Config, cc.createConfig.LanguageVariables)
		if err != nil {
			return err
		}
	} else {
		dockerfileTemplate.Config.VariableMapToDraftConfig(flagVariablesMap)

		if err = prompts.RunPromptsFromConfigWithSkips(dockerfileTemplate.Config); err != nil {
			return err
		}
	}

	if cc.templateVariableRecorder != nil {
		for _, variable := range dockerfileTemplate.Config.Variables {
			cc.templateVariableRecorder.Record(variable.Name, variable.Value)
		}
	}

	if err = dockerfileTemplate.Generate(); err != nil {
		return fmt.Errorf("there was an error when creating the Dockerfile for language %s: %w", cc.createConfig.LanguageType, err)
	}

	log.Info("--> Creating Dockerfile...\n")
	return nil
}

func (cc *createCmd) generateDeployment() error {
	log.Info("--- Deployment File Creation ---")
	var deployType string
	var deployTemplate *handlers.Template
	var err error

	if !interactive {
		if cc.createConfig.DeployType == "" {
			cc.createConfig.DeployType = "manifests"
			log.Debugf("using default deployment type %s", cc.createConfig.DeployType)
		}
	}
	if cc.createConfig.DeployType != "" {

		deployType = strings.ToLower(cc.createConfig.DeployType)
		deployTemplate, err = handlers.GetTemplate(fmt.Sprintf("deployment-%s", deployType), "", cc.dest, cc.templateWriter)
		if err != nil {
			return err
		}
		if deployTemplate == nil || deployTemplate.Config == nil {
			return errors.New("invalid deployment type")
		}
		deployTemplate.Config.VariableMapToDraftConfig(flagVariablesMap)
		if !interactive {
			currentDir, err := os.Getwd()
			if err != nil {
				return fmt.Errorf("getting current directory: %w", err)
			}

			defaultAppName := fmt.Sprintf("%s-workflow", filepath.Base(currentDir))
			if validName, err := ToValidAppName(defaultAppName); err != nil {
				log.Debugf("unable to convert default app name %q to a valid name: %v", defaultAppName, err)
				log.Debugf("using default app name %q", "my-app")
				defaultAppName = "my-app"
			} else {
				defaultAppName = validName
			}
			appVar, err := deployTemplate.Config.GetVariable("APPNAME")
			if err != nil || appVar == nil {
				log.Debugf("unable to get APP_NAME variable: %v", err)
			}
			if err == nil {
				appVar.Default.Value = defaultAppName
			}

			deployTemplate.Config.ApplyDefaultVariables()
		}
		err = validateConfigInputsToPrompts(deployTemplate.Config, cc.createConfig.DeployVariables)
		if err != nil {
			return err
		}
	} else {
		if cc.deployType == "" {
			selection := &promptui.Select{
				Label: "Select k8s Deployment Type",
				Items: []string{"manifests", "kustomize", "helm"},
			}

			_, deployType, err = selection.Run()
			if err != nil {
				return err
			}
		} else {
			deployType = cc.deployType
		}

		deployTemplate, err = handlers.GetTemplate(fmt.Sprintf("deployment-%s", deployType), "", cc.dest, cc.templateWriter)
		if err != nil {
			return err
		}

		if deployTemplate == nil || deployTemplate.Config == nil {
			return errors.New("invalid deployment type")
		}

		deployTemplate.Config.VariableMapToDraftConfig(flagVariablesMap)

		err = prompts.RunPromptsFromConfigWithSkips(deployTemplate.Config)
		if err != nil {
			return err
		}
	}

	if cc.templateVariableRecorder != nil {
		for _, variable := range deployTemplate.Config.Variables {
			cc.templateVariableRecorder.Record(variable.Name, variable.Value)
		}
	}

	log.Infof("--> Creating %s Kubernetes resources...\n", deployType)
	return deployTemplate.Generate()
}

func (cc *createCmd) createFiles(detectedLangTempalte *handlers.Template, lowerLang string) error {
	// does no further checks without file detection

	if cc.dockerfileOnly && cc.deploymentOnly {
		return errors.New("can only pass in one of --dockerfile-only and --deployment-only")
	}

	if cc.skipFileDetection {
		if !cc.deploymentOnly {
			err := cc.generateDockerfile(detectedLangTempalte, lowerLang)
			if err != nil {
				return err
			}
		}
		if !cc.dockerfileOnly {
			err := cc.generateDeployment()
			if err != nil {
				return err
			}
		}
		return nil
	}

	// check if the local directory has dockerfile or charts
	hasDockerFile, hasDeploymentFiles, err := filematches.SearchDirectory(cc.dest)
	if err != nil {
		return err
	}

	// prompts user for dockerfile re-creation
	if hasDockerFile && !cc.deploymentOnly {
		if !interactive && !cc.skipFileDetection {
			return fmt.Errorf("dockerfile already exists in the directory '%s', use --skip-file-detection to overwrite", cc.dest)
		}
		selection := &promptui.Select{
			Label: "We found Dockerfile in the directory, would you like to recreate the Dockerfile?",
			Items: []string{"yes", "no"},
		}

		_, selectResponse, err := selection.Run()
		if err != nil {
			return err
		}

		hasDockerFile = strings.EqualFold(selectResponse, "no")
	}

	if cc.deploymentOnly {
		log.Info("--> --deployment-only=true, skipping Dockerfile creation...")
	} else if hasDockerFile {
		log.Info("--> Found Dockerfile in local directory, skipping Dockerfile creation...")
	} else if !cc.deploymentOnly {
		err := cc.generateDockerfile(detectedLangTempalte, lowerLang)
		if err != nil {
			return err
		}
	}

	// prompts user for deployment re-creation
	if hasDeploymentFiles && !cc.dockerfileOnly {
		if !interactive && !cc.skipFileDetection {
			return fmt.Errorf("deployment files already exist in the directory '%s', use --skip-file-detection to overwrite", cc.dest)
		}
		selection := &promptui.Select{
			Label: "We found deployment files in the directory, would you like to create new deployment files?",
			Items: []string{"yes", "no"},
		}

		_, selectResponse, err := selection.Run()
		if err != nil {
			return err
		}

		hasDeploymentFiles = strings.EqualFold(selectResponse, "no")
	}

	if cc.dockerfileOnly {
		log.Info("--> --dockerfile-only=true, skipping deployment file creation...")
	} else if hasDeploymentFiles {
		log.Info("--> Found deployment directory in local directory, skipping deployment file creation...")
	} else if !cc.dockerfileOnly {
		err := cc.generateDeployment()
		if err != nil {
			return err
		}
	}

	log.Info("Draft has successfully created deployment resources for your project 😃")
	log.Info("Use 'draft setup-gh' to set up Github OIDC.")

	return nil
}

func init() {
	rootCmd.AddCommand(newCreateCmd())
}

func validateConfigInputsToPrompts(draftConfig *config.DraftConfig, provided []UserInputs) error {
	// set inputs to provided values
	for _, providedVar := range provided {
		draftConfig.SetVariable(providedVar.Name, providedVar.Value)
	}

	return nil
}

func promptLanguageSelection(supportedLanguages []string) ([]*linguist.Language, error) {
	selection := &promptui.Select{
		Label: "Unable to detect a supported language, please select one:",
		Items: supportedLanguages,
	}
	_, selectResponse, err := selection.Run()
	if err != nil {
		return nil, fmt.Errorf("manually selecting language: %w", err)
	}
	langs := []*linguist.Language{{Language: selectResponse}}
	return langs, nil
}
