package cmd

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"
	"text/tabwriter"

	"github.com/ory/viper"
	"github.com/spf13/cobra"

	"knative.dev/func/pkg/config"
	fn "knative.dev/func/pkg/functions"
)

// ErrTemplateRepoDoesNotExist is a sentinel error if a template repository responds with 404 status code
var ErrTemplateRepoDoesNotExist = errors.New("template repo does not exist")

func NewTemplatesCmd(newClient ClientFactory) *cobra.Command {
	cmd := &cobra.Command{
		Use:   "templates",
		Short: "List available function source templates",
		Long: `
NAME
	{{rootCmdUse}} templates - list available function source templates

SYNOPSIS
	{{rootCmdUse}} templates [language] [--json] [-r|--repository]

DESCRIPTION
	List all templates available, optionally for a specific language runtime.

	To specify a URI of a single, specific repository for which templates
	should be displayed, use the --repository flag.

	Installed repositories are by default located at ~/.func/repositories
	($XDG_CONFIG_HOME/.func/repositories).  This can be overridden with
	$FUNC_REPOSITORIES_PATH.

	To see all available language runtimes, see the 'languages' command.


EXAMPLES

	o Show a list of all available templates grouped by language runtime
	  $ {{rootCmdUse}} templates

	o Show a list of all templates for the Go runtime
	  $ {{rootCmdUse}} templates go

	o Return a list of all template runtimes in JSON output format
	  $ {{rootCmdUse}} templates --json

	o Return Go templates in a specific repository
		$ {{rootCmdUse}} templates go --repository=https://github.com/boson-project/templates
`,
		PreRunE: bindEnv("json", "repository", "verbose"),
		RunE: func(cmd *cobra.Command, args []string) error {
			return runTemplates(cmd, args, newClient)
		},
	}

	cfg, err := config.NewDefault()
	if err != nil {
		fmt.Fprintf(cmd.OutOrStdout(), "error loading config at '%v'. %v\n", config.File(), err)
	}

	cmd.Flags().Bool("json", false, "Set output to JSON format. (Env: $FUNC_JSON)")
	cmd.Flags().StringP("repository", "r", "", "URI to a specific repository to consider ($FUNC_REPOSITORY)")
	addVerboseFlag(cmd, cfg.Verbose)

	return cmd
}

func runTemplates(cmd *cobra.Command, args []string, newClient ClientFactory) (err error) {
	// Gather config
	cfg, err := newTemplatesConfig()
	if err != nil {
		return
	}

	// Simple ping to the repo to avoid subsequent errors from http package if it does not exist
	if cfg.Repository != "" {
		res, err := http.Get(cfg.Repository)
		if err != nil {
			return err
		}
		defer res.Body.Close()
		if res.StatusCode == http.StatusNotFound {
			return ErrTemplateRepoDoesNotExist
		}
	}

	// Client which will provide data
	client, done := newClient(
		ClientConfig{Verbose: cfg.Verbose},
		fn.WithRepository(cfg.Repository))
	defer done()

	// For a single language runtime
	// -------------------
	if len(args) == 1 {
		templates, err := client.Templates().List(args[0])
		if err != nil {
			return err
		}
		if cfg.JSON {
			s, err := json.MarshalIndent(templates, "", "  ")
			if err != nil {
				return err
			}
			fmt.Fprintln(cmd.OutOrStdout(), string(s))
		} else {
			for _, template := range templates {
				fmt.Fprintln(cmd.OutOrStdout(), template)
			}
		}
		return nil
	} else if len(args) > 1 {
		return errors.New("unexpected extra arguments")
	}

	// All language runtimes
	// ------------
	runtimes, err := client.Runtimes()
	if err != nil {
		return
	}
	if cfg.JSON {
		// Gather into a single data structure for printing as json
		templateMap := make(map[string][]string)
		for _, runtime := range runtimes {
			templates, err := client.Templates().List(runtime)
			if err != nil {
				return err
			}
			templateMap[runtime] = templates
		}
		s, err := json.MarshalIndent(templateMap, "", "  ")
		if err != nil {
			return err
		}
		fmt.Fprintln(cmd.OutOrStdout(), string(s))
	} else {
		// print using a formatted writer (sorted)
		builder := strings.Builder{}
		writer := tabwriter.NewWriter(&builder, 0, 0, 3, ' ', 0)
		fmt.Fprint(writer, "LANGUAGE\tTEMPLATE\n")
		for _, runtime := range runtimes {
			templates, err := client.Templates().List(runtime)
			if err != nil {
				return err
			}
			for _, template := range templates {
				fmt.Fprintf(writer, "%v\t%v\n", runtime, template)
			}
		}
		writer.Flush()
		fmt.Fprint(cmd.OutOrStdout(), builder.String())
	}
	return
}

type templatesConfig struct {
	Verbose    bool
	Repository string // Consider only a specific repository (URI)
	JSON       bool   // output as JSON
}

func newTemplatesConfig() (cfg templatesConfig, err error) {
	cfg = templatesConfig{
		Verbose:    viper.GetBool("verbose"),
		Repository: viper.GetString("repository"),
		JSON:       viper.GetBool("json"),
	}

	return
}
