package cli

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

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

// 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() {
	if log.GetLevel() <= log.InfoLevel {
		_, _ = fmt.Fprintln(mfa.Stdout, banner)
		_, _ = fmt.Fprintf(mfa.Stdout, "        mfer by @sneak: v%s released %s\n", mfer.Version, mfer.ReleaseDate)
		_, _ = fmt.Fprintln(mfa.Stdout, "            https://sneak.berlin/go/mfer")
	}
}

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

func (mfa *CLIApp) setVerbosity(c *cli.Context) {
	_, present := os.LookupEnv("MFER_DEBUG")
	if present {
		log.EnableDebugLogging()
	} else if c.Bool("quiet") {
		log.SetLevel(log.ErrorLevel)
	} else {
		log.SetLevelFromVerbosity(c.Count("verbose"))
	}
}

// commonFlags returns the flags shared by most commands (-v, -q)
func commonFlags() []cli.Flag {
	return []cli.Flag{
		&cli.BoolFlag{
			Name:    "verbose",
			Aliases: []string{"v"},
			Usage:   "Increase verbosity (-v for verbose, -vv for debug)",
			Count:   new(int),
		},
		&cli.BoolFlag{
			Name:    "quiet",
			Aliases: []string{"q"},
			Usage:   "Suppress output except errors",
		},
	}
}

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()

	mfa.app = &cli.App{
		Name:                 mfa.appname,
		Usage:                "Manifest generator",
		Version:              mfa.VersionString(),
		EnableBashCompletion: true,
		Writer:               mfa.Stdout,
		ErrWriter:            mfa.Stderr,
		Action: func(c *cli.Context) error {
			if c.Args().Len() > 0 {
				return fmt.Errorf("unknown command %q", c.Args().First())
			}
			mfa.printBanner()
			return cli.ShowAppHelp(c)
		},
		Commands: []*cli.Command{
			{
				Name:    "generate",
				Aliases: []string{"gen"},
				Usage:   "Generate manifest file",
				Action: func(c *cli.Context) error {
					mfa.setVerbosity(c)
					mfa.printBanner()
					return mfa.generateManifestOperation(c)
				},
				Flags: append(commonFlags(),
					&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",
					},
					&cli.StringFlag{
						Name:    "sign-key",
						Aliases: []string{"s"},
						Usage:   "GPG key ID to sign the manifest with",
						EnvVars: []string{"MFER_SIGN_KEY"},
					},
				),
			},
			{
				Name:      "check",
				Usage:     "Validate files using manifest file",
				ArgsUsage: "[manifest file]",
				Action: func(c *cli.Context) error {
					mfa.setVerbosity(c)
					mfa.printBanner()
					return mfa.checkManifestOperation(c)
				},
				Flags: append(commonFlags(),
					&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",
					},
					&cli.StringFlag{
						Name:    "require-signature",
						Aliases: []string{"S"},
						Usage:   "Require manifest to be signed by the specified GPG key ID",
						EnvVars: []string{"MFER_REQUIRE_SIGNATURE"},
					},
				),
			},
			{
				Name:      "freshen",
				Usage:     "Update manifest with changed, new, and removed files",
				ArgsUsage: "[manifest file]",
				Action: func(c *cli.Context) error {
					mfa.setVerbosity(c)
					mfa.printBanner()
					return mfa.freshenManifestOperation(c)
				},
				Flags: append(commonFlags(),
					&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",
					},
					&cli.StringFlag{
						Name:    "sign-key",
						Aliases: []string{"s"},
						Usage:   "GPG key ID to sign the manifest with",
						EnvVars: []string{"MFER_SIGN_KEY"},
					},
				),
			},
			{
				Name:  "version",
				Usage: "Show version",
				Action: func(c *cli.Context) error {
					_, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString())
					return nil
				},
			},
			{
				Name:      "list",
				Aliases:   []string{"ls"},
				Usage:     "List files in manifest",
				ArgsUsage: "[manifest file]",
				Action: func(c *cli.Context) error {
					return mfa.listManifestOperation(c)
				},
				Flags: []cli.Flag{
					&cli.BoolFlag{
						Name:    "long",
						Aliases: []string{"l"},
						Usage:   "Show size and mtime",
					},
					&cli.BoolFlag{
						Name:  "print0",
						Usage: "Separate entries with NUL character (for xargs -0)",
					},
				},
			},
			{
				Name:  "fetch",
				Usage: "fetch manifest and referenced files",
				Action: func(c *cli.Context) error {
					mfa.setVerbosity(c)
					mfa.printBanner()
					return mfa.fetchManifestOperation(c)
				},
				Flags: commonFlags(),
			},
		},
	}

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