package pkg

import (
	"flag"
	"fmt"
	"os"
	"strings"

	"gitlab.com/M0M097/pkg/cli"
)

// Command selects the top-level workflow that RunWithUI executes.
//
// A Settings value has exactly one command. NewSettings derives it from the
// mutually exclusive command-line flags; callers that construct Settings
// directly should use one of the Command* constants.
type Command int

const (
	// CommandUpgrade builds and installs Settings.PkgNames and, unless NoDeps
	// is set, their dependencies.
	CommandUpgrade Command = iota

	// CommandDependents lists the packages that depend on Settings.Root within
	// the dependency graph built from Settings.PkgNames.
	CommandDependents

	// CommandListInstalled lists packages from the repository that are already
	// installed under Config.DocDir.
	CommandListInstalled

	// CommandSearch lists packages matching Settings.PkgNames as search terms.
	CommandSearch

	// CommandShowVersion prints VERSION and exits before reading the repository.
	CommandShowVersion

	// CommandTracking lists Settings.PkgNames and their dependencies in build
	// order without building or installing them.
	CommandTracking

	// CommandView displays the README and info files for Settings.PkgNames.
	CommandView

	// CommandViewBuild displays the SlackBuild files for Settings.PkgNames.
	CommandViewBuild
)

// Settings contains the parsed command-line behavior consumed by RunWithUI.
type Settings struct {
	Update, Yes, UpdateBlacklist, CheckMD5Sum, RespectGreylist, FilterInstalled, NoDeps bool
	Root                                                                                string
	PkgNames                                                                            []string
	Jobs                                                                                int
	BuildOptions                                                                        map[string][]string

	// Cmd selects which top-level workflow RunWithUI executes. NewSettings sets
	// this from the mutually exclusive command flags, defaulting to
	// CommandUpgrade when no command flag is present.
	Cmd Command
}

// NewSettings parses the command line arguments and returns a Settings struct.
//
// The behaviour is described in the `man pkg` page and `pkg -h`.
func NewSettings(args []string, DefaultPkgFile string) (Settings, error) {
	var (
		fs                 = flag.NewFlagSet(args[0], flag.ExitOnError)
		filterInstalledPtr = fs.Bool(FILTERINSTALLED_FLAG, false, "don't filter out installed packages")
		filePtr            = fs.String(FILE_FLAG, DefaultPkgFile, "path to the input file")
		jobsPtr            = fs.Int(JOBS_FLAG, NUM_DEFAULT_JOBS, "maximum number of package builds to run in parallel")
		listInstalledPtr   = fs.Bool(LIST_INSTALLED_FLAG, false, "list installed packages")
		md5Ptr             = fs.Bool(MD5_FLAG, false, "skip MD5Sum checks")
		noDepsPtr          = fs.Bool(NO_DEPS_FLAG, false, "don't install dependencies")
		rootPtr            = fs.String(ROOT_FLAG, "", "display dependents within the dependency graph")
		skipGreylistPtr    = fs.Bool(RESPECTGREYLIST_FLAG, false, "ignore the greylist")
		searchPtr          = fs.Bool(SEARCH_FLAG, false, "search for a package")
		trackPtr           = fs.Bool(TRACK_FLAG, false, "track the given packages (show all dependencies)")
		updatePtr          = fs.Bool(UPDATE_FLAG, false, "update the repository")
		versionPtr         = fs.Bool(VERSION_FLAG, false, "display the version of pkg")
		viewPtr            = fs.Bool(VIEW_FLAG, false, "display information for the given packages")
		viewBuildPtr       = fs.Bool(VIEWBUILD_FLAG, false, "display build scripts for the given packages")
		yesPtr             = fs.Bool(YES_FLAG, false, "Skip all prompts")
	)

	fs.Usage = func() {
		fmt.Fprintf(fs.Output(), "See `%sman pkg%s`for usage guidance.\n", cli.Blue, cli.Reset)
		fs.PrintDefaults()
	}

	fs.Parse(args[1:])

	if *jobsPtr < 1 {
		return Settings{}, fmt.Errorf("-%s must be at least 1, got %d", JOBS_FLAG, *jobsPtr)
	}

	if err := checkMutuallyExclusiveFlags(
		fs, VERSION_FLAG, VIEW_FLAG, VIEWBUILD_FLAG, TRACK_FLAG, SEARCH_FLAG, LIST_INSTALLED_FLAG, ROOT_FLAG, NO_DEPS_FLAG,
	); err != nil {
		return Settings{}, err
	}

	var cmd Command
	switch {
	case *listInstalledPtr:
		cmd = CommandListInstalled
	case *rootPtr != "":
		cmd = CommandDependents
	case *searchPtr:
		cmd = CommandSearch
	case *versionPtr:
		cmd = CommandShowVersion
	case *trackPtr:
		cmd = CommandTracking
	case *viewPtr:
		cmd = CommandView
	case *viewBuildPtr:
		cmd = CommandViewBuild
	default:
		cmd = CommandUpgrade
	}

	pkgNames, buildOptions := pkgNamesAndBuildOptions(fs.Args())
	pkgs_from_file := len(pkgNames) == 0
	switch {
	case *versionPtr: // We only need to display the version anyway
	case pkgs_from_file: // Read the list of packages to upgrade
		f, err := os.Open(*filePtr)
		if err != nil {
			return Settings{}, fmt.Errorf("%w\n\nThis is probably your first usage of `pkg` read `man pkg`.", err)
		}
		defer f.Close()
		pkgNames, buildOptions = pkgNamesAndBuildOptions(cli.StripCommentsAndTokenize(f))
	case *filePtr != DefaultPkgFile:
		return Settings{}, fmt.Errorf("Cannot use positional arguments and -%s together", FILE_FLAG)
	}

	return Settings{
		BuildOptions:    buildOptions,
		Cmd:             cmd,
		CheckMD5Sum:     !*md5Ptr,
		NoDeps:          *noDepsPtr,
		FilterInstalled: !*filterInstalledPtr,
		RespectGreylist: !*skipGreylistPtr,
		Root:            *rootPtr,
		PkgNames:        pkgNames,
		Jobs:            *jobsPtr,
		UpdateBlacklist: (*filePtr == DefaultPkgFile) && pkgs_from_file && !*noDepsPtr,
		Update:          *updatePtr,
		Yes:             *yesPtr,
	}, nil
}

// checkMutuallyExclusiveFlags returns an error if more than one of the given
// flags are used.
func checkMutuallyExclusiveFlags(fs *flag.FlagSet, flags ...string) error {
	var used []string
	fs.Visit(func(f *flag.Flag) {
		for _, flag := range flags {
			if f.Name == flag {
				used = append(used, "-"+f.Name)
				break
			}
		}
	})
	if len(used) > 1 {
		return fmt.Errorf("At most one of %s can be used at a time", strings.Join(used, ", "))
	}
	return nil
}

// PkgNamesAndBuildOptions parses the package names and build options from a
// slice of strings. The input slice is expected to contain package names
// followed by build options in the format "name=option".
func pkgNamesAndBuildOptions(s []string) ([]string, map[string][]string) {
	buildOptions := make(map[string][]string)
	pkgNames := make([]string, 0)
	for i := 0; i < len(s); {
		name := s[i]
		pkgNames = append(pkgNames, name)
		for i++; i < len(s) && strings.Contains(s[i], "="); i++ {
			buildOptions[name] = append(buildOptions[name], s[i])
		}
	}
	return pkgNames, buildOptions
}
