package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"os/exec"
	"sort"
	"strings"
	"text/template"

	"github.com/mitchellh/go-homedir"
	"github.com/pkg/errors"
	"github.com/symfony-cli/console"
	"github.com/symfony-cli/symfony-cli/local/platformsh"
)

type application struct {
	Namespaces []namespace
	Commands   []command
}

type namespace struct {
	ID       string
	Commands []string
}

type command struct {
	Name        string
	Usage       []string
	Description string
	Help        string
	Definition  definition
	Hidden      bool
	Aliases     []string
}

type definition struct {
	Arguments map[string]argument
	Options   map[string]option
}

type argument struct {
}

type option struct {
	Shortcut string
	Default  interface{}
}

var commandsTemplate = template.Must(template.New("output").Parse(`// Code generated by platformsh/generator/main.go
// DO NOT EDIT

/*
 * Copyright (c) 2021-present Fabien Potencier <fabien@symfony.com>
 *
 * This file is part of Symfony CLI project
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package platformsh

import (
	"github.com/symfony-cli/console"
)

var Commands = []*console.Command{
{{ .Definition -}}
}
`))

func generateCommands() {
	home, err := homedir.Dir()
	if err != nil {
		panic(err)
	}
	// as platform.sh and upsun have the same commands, we can use either one
	cloudPath, err := platformsh.Install(home, platformsh.PlatformshBrand)
	if err != nil {
		panic(err.Error())
	}
	definitionAsString, err := parseCommands(cloudPath)
	if err != nil {
		panic(err.Error())
	}
	data := map[string]interface{}{
		"Definition": definitionAsString,
	}
	var buf bytes.Buffer
	if err := commandsTemplate.Execute(&buf, data); err != nil {
		panic(err)
	}
	f, err := os.Create("local/platformsh/commands.go")
	if err != nil {
		panic(err)
	}
	f.Write(buf.Bytes())

}

func parseCommands(cloudPath string) (string, error) {
	var buf bytes.Buffer
	var bufErr bytes.Buffer
	cmd := exec.Command(cloudPath, "list", "--format=json", "--all")
	cmd.Stdout = &buf
	cmd.Stderr = &bufErr
	if err := cmd.Run(); err != nil {
		return "", errors.Errorf("unable to list commands: %s\n%s\n%s", err, bufErr.String(), buf.String())
	}

	// Fix PHP types
	cleanOutput := bytes.ReplaceAll(buf.Bytes(), []byte(`"arguments":[]`), []byte(`"arguments":{}`))

	var definition application
	if err := json.Unmarshal(cleanOutput, &definition); err != nil {
		return "", err
	}

	excludedCommands := map[string]bool{
		"list":              true,
		"help":              true,
		"self:stats":        true,
		"decode":            true,
		"environment:drush": true,
		"project:init":      true,
	}

	excludedOptions := console.AnsiFlag.Names()
	excludedOptions = append(excludedOptions, console.NoAnsiFlag.Names()...)
	excludedOptions = append(excludedOptions, console.NoInteractionFlag.Names()...)
	excludedOptions = append(excludedOptions, console.QuietFlag.Names()...)
	excludedOptions = append(excludedOptions, console.LogLevelFlag.Names()...)
	excludedOptions = append(excludedOptions, console.HelpFlag.Names()...)
	excludedOptions = append(excludedOptions, console.VersionFlag.Names()...)

	definitionAsString := ""
	for _, command := range definition.Commands {
		if strings.Contains(command.Description, "deprecated") || strings.Contains(command.Description, "DEPRECATED") {
			continue
		}
		if _, ok := excludedCommands[command.Name]; ok {
			continue
		}
		if strings.HasPrefix(command.Name, "local:") {
			continue
		}
		if strings.HasPrefix(command.Name, "self:") {
			command.Hidden = true
		}
		namespace := "cloud"
	loop:
		for _, n := range definition.Namespaces {
			for _, name := range n.Commands {
				if name == command.Name {
					if n.ID != "_global" {
						namespace += ":" + n.ID
					}
					break loop
				}
			}
		}
		name := strings.TrimPrefix("cloud:"+command.Name, namespace+":")
		aliases := []string{}
		if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") {
			aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", command.Name))
		}

		cmdAliases, err := getCommandAliases(command.Name, cloudPath)
		if err != nil {
			return "", err
		}
		aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", command.Name))
		for _, alias := range cmdAliases {
			aliases = append(aliases, fmt.Sprintf("{Name: \"cloud:%s\"}", alias))
			aliases = append(aliases, fmt.Sprintf("{Name: \"upsun:%s\", Hidden: true}", alias))
			if namespace != "cloud" && !strings.HasPrefix(command.Name, "self:") {
				aliases = append(aliases, fmt.Sprintf("{Name: \"%s\", Hidden: true}", alias))
			}
		}
		if command.Name == "environment:push" {
			aliases = append(aliases, "{Name: \"deploy\"}")
			aliases = append(aliases, "{Name: \"cloud:deploy\"}")
			aliases = append(aliases, "{Name: \"upsun:deploy\", Hidden: true}")
		}
		aliasesAsString := ""
		if len(aliases) > 0 {
			aliasesAsString += "\n\t\tAliases:  []*console.Alias{\n"
			for _, alias := range aliases {
				aliasesAsString += "\t\t\t" + alias + ",\n"
			}
			aliasesAsString += "\t\t},"
		}
		hide := ""
		if command.Hidden {
			hide = "\n\t\tHidden:   console.Hide,"
		}

		optionNames := make([]string, 0, len(command.Definition.Options))

	optionsLoop:
		for name := range command.Definition.Options {
			if name == "yes" || name == "no" || name == "version" {
				continue
			}
			for _, excludedOption := range excludedOptions {
				if excludedOption == name {
					continue optionsLoop
				}
			}

			optionNames = append(optionNames, name)
		}
		sort.Strings(optionNames)
		flags := []string{}
		for _, name := range optionNames {
			option := command.Definition.Options[name]
			optionAliasesAsString := ""
			if option.Shortcut != "" {
				optionAliasesAsString += " Aliases: []string{\""
				optionAliasesAsString += strings.Join(strings.Split(strings.ReplaceAll(option.Shortcut, "-", ""), "|"), "\", \"")
				optionAliasesAsString += "\"},"
			}
			flagType := "String"
			defaultValue := ""
			if value, ok := option.Default.(bool); ok {
				flagType = "Bool"
				if value {
					defaultValue = "true"
				}
			} else if value, ok := option.Default.(string); ok {
				defaultValue = fmt.Sprintf("%#v", value)
			}
			defaultValueAsString := ""
			if defaultValue != "" {
				defaultValueAsString = fmt.Sprintf(" DefaultValue: %s,", defaultValue)
			}
			flags = append(flags, fmt.Sprintf(`&console.%sFlag{Name: "%s",%s%s}`, flagType, name, optionAliasesAsString, defaultValueAsString))
		}
		flagsAsString := ""
		if len(flags) > 0 {
			flagsAsString += "\n\t\tFlags:    []console.Flag{\n"
			for _, flag := range flags {
				flagsAsString += "\t\t\t" + flag + ",\n"
			}
			flagsAsString += "\t\t},"
		}

		command.Description = strings.ReplaceAll(command.Description, "Platform.sh", "Platform.sh/Upsun")
		definitionAsString += fmt.Sprintf(`	{
		Category: "%s",
		Name:     "%s",%s
		Usage:    %#v,%s%s
	},
`, namespace, name, aliasesAsString, command.Description, hide, flagsAsString)
	}

	return definitionAsString, nil
}

func getCommandAliases(name, cloudPath string) ([]string, error) {
	var buf bytes.Buffer
	var bufErr bytes.Buffer
	c := exec.Command(cloudPath, name, "--help", "--format=json")
	c.Stdout = &buf
	c.Stderr = &bufErr
	if err := c.Run(); err != nil {
		// Can currently happen for commands implemented in Go upstream (like app:config-validate)
		// FIXME: to be removed once upstream implements --help --format=json for all commands
		return []string{}, nil
		//return nil, errors.Errorf("unable to get definition for command %s: %s\n%s\n%s", name, err, bufErr.String(), buf.String())
	}
	var cmd command
	if err := json.Unmarshal(buf.Bytes(), &cmd); err != nil {
		return nil, err
	}
	return cmd.Aliases, nil
}
