package cli

import (
	"errors"
	"fmt"
	"io"
	"os"
	"os/signal"
	"path"
	"time"
)

// App is the main structure of a cli application. It is recommended that
// an app be created with the cli.NewApp() function
type App struct {
	// The name of the program. Defaults to path.Base(os.Args[0])
	Name string
	// A short description of the usage of this command
	Usage string
	// Custom text to show on USAGE section of help
	UsageText string
	// A longer explanation of how the command works
	Description string
	// A short description of the arguments of this command
	ArgsUsage string
	// Compilation date
	Compiled time.Time
	// List of all authors who contributed
	Authors []*Author
	// Copyright of the binary if any
	Copyright string
	// Full name of command for help, defaults to full command name, including parent commands.
	HelpName string
	// Boolean to hide built-in help command
	HideHelp bool
	// Version of the program
	Version string
	// Boolean to hide built-in version flag and the VERSION section of help
	HideVersion bool
	// Signals are the signals that we want to handle
	Signals []os.Signal
	// List of commands to execute
	Commands []*Command
	// List of flags to parse
	Flags []Flag
	// Providers contains a list of all providers
	Providers []Provider
	// An action to execute before any subcommands are run, but after the context is ready
	// If a non-nil error is returned, no subcommands are run
	Before BeforeFunc
	// An action to execute after any subcommands are run, but after the subcommand has finished
	// It is run even if Action() panics
	After AfterFunc
	// An action to execute before provider execution
	BeforeInit BeforeFunc
	// An action to execute after provider execution
	AfterInit AfterFunc
	// The action to execute when no subcommands are specified
	// Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}`
	Action ActionFunc
	// Strategy enables comman retry logic
	Strategy BackOffStrategy
	// OnSignal occurs on system signal
	OnSignal SignalFunc
	// Execute this function if a usage error occurs.
	OnUsageError UsageErrorFunc
	// OnCommandNotFound is executed if the proper command cannot be found
	OnCommandNotFound CommandNotFoundFunc
	// Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to
	// function as a default, so this is optional.
	OnExitError ExitErrorHandlerFunc
	// Exit is the function used when the app exits. If not set defaults to os.Exit.
	Exit ExitFunc
	// Writer writer to write output to
	Writer io.Writer
	// ErrWriter writes error output
	ErrWriter io.Writer
}

// Run is the entry point to the cli app. Parses the arguments slice and routes
// to the proper flag/args combination
func (app *App) Run(args []string) {
	args = app.prepare(args)

	cmd := &Command{
		Name:              app.Name,
		Usage:             app.Usage,
		UsageText:         app.UsageText,
		HideHelp:          app.HideHelp,
		HelpName:          app.HelpName,
		Commands:          app.Commands,
		Description:       app.Description,
		ArgsUsage:         app.ArgsUsage,
		Flags:             app.Flags,
		Before:            app.Before,
		After:             app.After,
		BeforeInit:        app.BeforeInit,
		AfterInit:         app.AfterInit,
		Action:            app.Action,
		Strategy:          app.Strategy,
		Providers:         app.Providers,
		OnUsageError:      app.OnUsageError,
		OnCommandNotFound: app.OnCommandNotFound,
		Metadata: Map{
			"HideVersion": app.HideVersion,
			"Version":     app.Version,
			"Authors":     app.Authors,
			"Copyright":   app.Copyright,
		},
	}

	ctx := &Context{
		Command:   cmd,
		Args:      args[1:],
		Writer:    app.Writer,
		ErrWriter: app.ErrWriter,
		Metadata:  make(map[string]interface{}),
	}

	app.notify(ctx)
	app.error(cmd.RunWithContext(ctx))
}

func (app *App) notify(ctx *Context) {
	if len(app.Signals) == 0 {
		return
	}

	if app.OnSignal == nil {
		return
	}

	ch := make(chan os.Signal, 1)
	signal.Notify(ch, app.Signals...)

	go func() {
		signal := <-ch

		err := app.OnSignal(ctx, signal)
		app.error(err)
	}()
}

func (app *App) prepare(args []string) []string {
	app.flags()
	app.commands()
	return app.app(args)
}

func (app *App) app(args []string) []string {
	if len(args) == 0 {
		args = []string{"unknown"}
	}

	if app.Name == "" {
		app.Name = path.Base(args[0])
	}

	if app.Compiled.IsZero() {
		info, err := os.Stat(args[0])

		if err != nil {
			app.Compiled = time.Now()
		} else {
			app.Compiled = info.ModTime()
		}
	}

	if app.Writer == nil {
		app.Writer = os.Stdout
	}

	if app.ErrWriter == nil {
		app.ErrWriter = os.Stderr
	}

	if app.Exit == nil {
		app.Exit = os.Exit
	}

	return args
}

func (app *App) flags() {
	if !app.HideVersion {
		version := &BoolFlag{
			Name:  "version, v",
			Usage: "prints the version",
		}

		app.Flags = append(app.Flags, version)
	}
}

func (app *App) commands() {
	if !app.HideVersion {
		version := NewVersionCommand()
		app.Commands = append(app.Commands, version)
	}
}

func (app *App) error(err error) {
	if err == nil {
		return
	}

	if app.OnExitError != nil {
		err = app.OnExitError(err)
	}

	fmt.Fprintln(app.ErrWriter, err)

	var errx *ExitError

	if !errors.As(err, &errx) {
		errx = WrapError(err)
	}

	app.Exit(errx.Code())
}

// Author represents someone who has contributed to a cli project.
type Author struct {
	// Name of the author
	Name string
	// Email of the author
	Email string
}

// String makes Author comply to the Stringer interface, to allow an easy print in the templating process
func (author *Author) String() string {
	value := ""

	if author.Email != "" {
		value = fmt.Sprintf(" <%s>", author.Email)
	}

	return fmt.Sprintf("%v%v", author.Name, value)
}

// Copyright creates a copyright message
func Copyright(name string) string {
	return fmt.Sprintf("%s (C) %d", name, time.Now().Year())
}