package cli

import (
	"fmt"
	"io"
	"os"
	"time"

	"github.com/spf13/afero"
	"github.com/urfave/cli/v2"
	"sneak.berlin/go/mfer/internal/log"
)

// CLIApp is the main CLI application container. It holds configuration,
// I/O streams, and filesystem abstraction to enable testing and flexibility.
type CLIApp struct {
	appname     string
	version     string
	gitrev      string
	startupTime time.Time
	exitCode    int
	app         *cli.App

	Stdin  io.Reader // Standard input stream
	Stdout io.Writer // Standard output stream for normal output
	Stderr io.Writer // Standard error stream for diagnostics
	Fs     afero.Fs  // Filesystem abstraction for all file operations
}

const banner = `
     ___           ___         ___           ___
    /__/\         /  /\       /  /\         /  /\
   |  |::\       /  /:/_     /  /:/_       /  /::\
   |  |:|:\     /  /:/ /\   /  /:/ /\     /  /:/\:\
 __|__|:|\:\   /  /:/ /:/  /  /:/ /:/_   /  /:/~/:/
/__/::::| \:\ /__/:/ /:/  /__/:/ /:/ /\ /__/:/ /:/___
\  \:\~~\__\/ \  \:\/:/   \  \:\/:/ /:/ \  \:\/:::::/
 \  \:\        \  \::/     \  \::/ /:/   \  \::/~~~~
  \  \:\        \  \:\      \  \:\/:/     \  \:\
   \  \:\        \  \:\      \  \::/       \  \:\
    \__\/         \__\/       \__\/         \__\/`

func (mfa *CLIApp) printBanner() {
	_, _ = fmt.Fprintln(mfa.Stdout, banner)
}

// VersionString returns the version and git revision formatted for display.
func (mfa *CLIApp) VersionString() string {
	return fmt.Sprintf("%s (%s)", mfa.version, mfa.gitrev)
}

func (mfa *CLIApp) setVerbosity(v int) {
	_, present := os.LookupEnv("MFER_DEBUG")
	if present {
		log.EnableDebugLogging()
	} else {
		log.SetLevelFromVerbosity(v)
	}
}

func (mfa *CLIApp) run(args []string) {
	mfa.startupTime = time.Now()

	if NO_COLOR {
		// shoutout to rob pike who thinks it's juvenile
		log.DisableStyling()
	}

	// Configure log package to use our I/O streams
	log.SetOutput(mfa.Stdout, mfa.Stderr)
	log.Init()

	var verbosity int

	mfa.app = &cli.App{
		Name:                 mfa.appname,
		Usage:                "Manifest generator",
		Version:              mfa.VersionString(),
		EnableBashCompletion: true,
		Writer:               mfa.Stdout,
		ErrWriter:            mfa.Stderr,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:    "verbose",
				Usage:   "Verbosity level",
				Aliases: []string{"v"},
				Count:   &verbosity,
			},
			&cli.BoolFlag{
				Name:    "quiet",
				Usage:   "don't produce output except on error",
				Aliases: []string{"q"},
			},
		},
		Commands: []*cli.Command{
			{
				Name:    "generate",
				Aliases: []string{"gen"},
				Usage:   "Generate manifest file",
				Action: func(c *cli.Context) error {
					if !c.Bool("quiet") {
						mfa.printBanner()
					}
					mfa.setVerbosity(verbosity)
					return mfa.generateManifestOperation(c)
				},
				Flags: []cli.Flag{
					&cli.BoolFlag{
						Name:    "FollowSymLinks",
						Aliases: []string{"follow-symlinks"},
						Usage:   "Resolve encountered symlinks",
					},
					&cli.BoolFlag{
						Name:    "IncludeDotfiles",
						Aliases: []string{"include-dotfiles"},
						Usage:   "Include dot (hidden) files (excluded by default)",
					},
					&cli.StringFlag{
						Name:    "output",
						Value:   "./.index.mf",
						Aliases: []string{"o"},
						Usage:   "Specify output filename",
					},
					&cli.BoolFlag{
						Name:    "force",
						Aliases: []string{"f"},
						Usage:   "Overwrite output file if it exists",
					},
					&cli.BoolFlag{
						Name:    "progress",
						Aliases: []string{"P"},
						Usage:   "Show progress during enumeration and scanning",
					},
				},
			},
			{
				Name:      "check",
				Usage:     "Validate files using manifest file",
				ArgsUsage: "[manifest file]",
				Action: func(c *cli.Context) error {
					if !c.Bool("quiet") {
						mfa.printBanner()
					}
					mfa.setVerbosity(verbosity)
					return mfa.checkManifestOperation(c)
				},
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name:    "base",
						Aliases: []string{"b"},
						Value:   ".",
						Usage:   "Base directory for resolving relative paths from manifest",
					},
					&cli.BoolFlag{
						Name:    "progress",
						Aliases: []string{"P"},
						Usage:   "Show progress during checking",
					},
					&cli.BoolFlag{
						Name:  "no-extra-files",
						Usage: "Fail if files exist in base directory that are not in manifest",
					},
				},
			},
			{
				Name:      "freshen",
				Usage:     "Update manifest with changed, new, and removed files",
				ArgsUsage: "[manifest file]",
				Action: func(c *cli.Context) error {
					if !c.Bool("quiet") {
						mfa.printBanner()
					}
					mfa.setVerbosity(verbosity)
					return mfa.freshenManifestOperation(c)
				},
				Flags: []cli.Flag{
					&cli.StringFlag{
						Name:    "base",
						Aliases: []string{"b"},
						Value:   ".",
						Usage:   "Base directory for resolving relative paths",
					},
					&cli.BoolFlag{
						Name:    "FollowSymLinks",
						Aliases: []string{"follow-symlinks"},
						Usage:   "Resolve encountered symlinks",
					},
					&cli.BoolFlag{
						Name:    "IncludeDotfiles",
						Aliases: []string{"include-dotfiles"},
						Usage:   "Include dot (hidden) files (excluded by default)",
					},
					&cli.BoolFlag{
						Name:    "progress",
						Aliases: []string{"P"},
						Usage:   "Show progress during scanning and hashing",
					},
				},
			},
			{
				Name:  "version",
				Usage: "Show version",
				Action: func(c *cli.Context) error {
					_, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString())
					return nil
				},
			},
			{
				Name:  "fetch",
				Usage: "fetch manifest and referenced files",
				Action: func(c *cli.Context) error {
					if !c.Bool("quiet") {
						mfa.printBanner()
					}
					mfa.setVerbosity(verbosity)
					return mfa.fetchManifestOperation(c)
				},
			},
		},
	}

	mfa.app.HideVersion = true
	err := mfa.app.Run(args)
	if err != nil {
		mfa.exitCode = 1
		log.WithError(err).Debugf("exiting")
	}
}
