package profilescmdline

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

	"v.io/jiri"
	"v.io/jiri/profiles"
	"v.io/jiri/profiles/profilesmanager"
	"v.io/x/lib/cmdline"
	"v.io/x/lib/lookpath"
)

func newCmdOSPackages() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runPackages),
		Name:     "os-packages",
		Short:    "List the commands to install the OS packages required by the given profiles",
		Long:     "List or optionally run the commands to install the OS packages required by the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to list OS packages for.",
	}
}

func newCmdInstall() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runInstall),
		Name:     "install",
		Short:    "Install the given profiles",
		Long:     "Install the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to install.",
	}
}

func newCmdUninstall() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runUninstall),
		Name:     "uninstall",
		Short:    "Uninstall the given profiles",
		Long:     "Uninstall the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to uninstall.",
	}
}

func newCmdUpdate() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runUpdate),
		Name:     "update",
		Short:    "Install the latest default version of the given profiles",
		Long:     "Install the latest default version of the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to update, if omitted all profiles are updated.",
	}
}

func newCmdCleanup() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runCleanup),
		Name:     "cleanup",
		Short:    "Cleanup the locally installed profiles",
		Long:     "Cleanup the locally installed profiles. This is generally required when recovering from earlier bugs or when preparing for a subsequent change to the profiles implementation.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to cleanup, if omitted all profiles are cleaned.",
	}
}

func newCmdAvailable() *cmdline.Command {
	return &cmdline.Command{
		Runner: jiri.RunnerFunc(runAvailable),
		Name:   "available",
		Short:  "List the available profiles",
		Long:   "List the available profiles.",
	}
}

func runUpdate(jirix *jiri.X, args []string) error {
	return updateImpl(jirix, &updateFlags, args)
}

func runCleanup(jirix *jiri.X, args []string) error {
	return cleanupImpl(jirix, &cleanupFlags, args)
}

func runPackages(jirix *jiri.X, args []string) error {
	return packagesImpl(jirix, &packagesFlags, args)
}

func runInstall(jirix *jiri.X, args []string) error {
	return installImpl(jirix, &installFlags, args)
}

func runUninstall(jirix *jiri.X, args []string) error {
	return uninstallImpl(jirix, &uninstallFlags, args)
}

func runAvailable(jirix *jiri.X, args []string) error {
	return availableImpl(jirix, &availableFlags, args)
}

type commonFlagValues struct {

	dbPath string

	root string
}

func initCommon(flags *flag.FlagSet, c *commonFlagValues, installer, defaultDBPath, defaultProfilesPath string) {
	RegisterDBPathFlag(flags, &c.dbPath, defaultDBPath)
	flags.StringVar(&c.root, "profiles-dir", defaultProfilesPath, "the directory, relative to JIRI_ROOT, that profiles are installed in")
}

func (cv *commonFlagValues) args() []string {
	a := append([]string{}, "--profiles-db="+cv.dbPath)
	a = append(a, "--profiles-dir="+cv.root)
	return a
}

type packagesFlagValues struct {
	commonFlagValues

	target profiles.Target

	allPackages bool

	installPackages bool
}

func initPackagesCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &packagesFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetAndEnvFlags(flags, &packagesFlags.target)
	flags.BoolVar(&packagesFlags.allPackages, "all", false, "print commands to install all required OS packages, not just those that are missing")
	flags.BoolVar(&packagesFlags.installPackages, "install", false, "install the requested packages. This may need to be run as root.")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Install)
	}
}

func (pv *packagesFlagValues) args() []string {
	a := pv.commonFlagValues.args()
	if t := pv.target.String(); t != "" {
		a = append(a, "--target="+t)
	}
	if e := pv.target.CommandLineEnv().String(); e != "" {
		a = append(a, "--target="+e)
	}
	a = append(a, fmt.Sprintf("--%s=%v", "all", pv.allPackages))
	return append(a, fmt.Sprintf("--%s=%v", "install", pv.installPackages))
}

type installFlagValues struct {
	commonFlagValues

	target profiles.Target

	force bool
}

func initInstallCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &installFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetAndEnvFlags(flags, &installFlags.target)
	flags.BoolVar(&installFlags.force, "force", false, "force install the profile even if it is already installed")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Install)
	}
}

func (iv *installFlagValues) args() []string {
	a := iv.commonFlagValues.args()
	if t := iv.target.String(); t != "" {
		a = append(a, "--target="+t)
	}
	if e := iv.target.CommandLineEnv().String(); e != "" {
		a = append(a, "--target="+e)
	}
	return append(a, fmt.Sprintf("--%s=%v", "force", iv.force))
}

type uninstallFlagValues struct {
	commonFlagValues

	target profiles.Target

	allTargets bool

	verbose bool

}

func initUninstallCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &uninstallFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetFlag(flags, &uninstallFlags.target)
	flags.BoolVar(&uninstallFlags.allTargets, "all-targets", false, "apply to all targets for the specified profile(s)")
	flags.BoolVar(&uninstallFlags.verbose, "v", false, "print more detailed information")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Uninstall)
	}
}

func (uv *uninstallFlagValues) args() []string {
	a := uv.commonFlagValues.args()
	if uv.target.String() != "" {
		a = append(a, "--target="+uv.target.String())
	}
	a = append(a, fmt.Sprintf("--%s=%v", "all-targets", uv.allTargets))
	return append(a, fmt.Sprintf("--%s=%v", "v", uv.verbose))
}

type cleanupFlagValues struct {
	commonFlagValues

	gc bool

	rewriteDB bool

	rmAll bool

	verbose bool
}

func initCleanupCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &cleanupFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	flags.BoolVar(&cleanupFlags.gc, "gc", false, "uninstall profile targets that are older than the current default")
	flags.BoolVar(&cleanupFlags.rmAll, "rm-all", false, "remove profiles database and all profile generated output files.")
	flags.BoolVar(&cleanupFlags.rewriteDB, "rewrite-profiles-db", false, "rewrite the profiles database to use the latest schema version")
	flags.BoolVar(&cleanupFlags.verbose, "v", false, "print more detailed information")
}

func (cv *cleanupFlagValues) args() []string {
	return append(cv.commonFlagValues.args(),
		fmt.Sprintf("--%s=%v", "gc", cv.gc),
		fmt.Sprintf("--%s=%v", "rewrite-profiles-db", cv.rewriteDB),
		fmt.Sprintf("--%s=%v", "v", cv.verbose),
		fmt.Sprintf("--%s=%v", "rm-all", cv.rmAll))
}

type updateFlagValues struct {
	commonFlagValues

	verbose bool
}

func initUpdateCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &updateFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	flags.BoolVar(&updateFlags.verbose, "v", false, "print more detailed information")
}

func (uv *updateFlagValues) args() []string {
	return append(uv.commonFlagValues.args(), fmt.Sprintf("--%s=%v", "v", uv.verbose))
}

type availableFlagValues struct {

	verbose bool

	describe bool
}

func initAvailableCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	flags.BoolVar(&availableFlags.verbose, "v", false, "print more detailed information")
	flags.BoolVar(&availableFlags.describe, "describe", false, "print the profile description")
}

func (av *availableFlagValues) args() []string {
	return []string{
		fmt.Sprintf("--%s=%v", "v", av.verbose),
		fmt.Sprintf("--%s=%v", "describe", av.describe),
	}
}

var (
	packagesFlags    packagesFlagValues
	installFlags     installFlagValues
	uninstallFlags   uninstallFlagValues
	cleanupFlags     cleanupFlagValues
	updateFlags      updateFlagValues
	availableFlags   availableFlagValues
	profileInstaller string
	runSubcommands   bool
)

func RegisterManagementCommands(parent *cmdline.Command, useSubcommands bool, installer, defaultDBPath, defaultProfilesPath string) {
	cmdOSPackages := newCmdOSPackages()
	cmdInstall := newCmdInstall()
	cmdUninstall := newCmdUninstall()
	cmdUpdate := newCmdUpdate()
	cmdCleanup := newCmdCleanup()
	cmdAvailable := newCmdAvailable()
	initPackagesCommand(&cmdOSPackages.Flags, installer, defaultDBPath, defaultProfilesPath)
	initInstallCommand(&cmdInstall.Flags, installer, defaultDBPath, defaultProfilesPath)
	initUninstallCommand(&cmdUninstall.Flags, installer, defaultDBPath, defaultProfilesPath)
	initUpdateCommand(&cmdUpdate.Flags, installer, defaultDBPath, defaultProfilesPath)
	initCleanupCommand(&cmdCleanup.Flags, installer, defaultDBPath, defaultProfilesPath)
	initAvailableCommand(&cmdAvailable.Flags, installer, defaultDBPath, defaultProfilesPath)
	parent.Children = append(parent.Children, cmdInstall, cmdOSPackages, cmdUninstall, cmdUpdate, cmdCleanup, cmdAvailable)
	profileInstaller = installer
	runSubcommands = useSubcommands
}

func findProfileSubcommands(jirix *jiri.X) []string {
	if !runSubcommands {
		return nil
	}
	cmds, _ := lookpath.LookPrefix(jirix.Env(), "jiri-profile-", nil)
	return cmds
}

func allAvailableManagers(jirix *jiri.X) ([]string, error) {
	names := profilesmanager.Managers()
	if profileInstaller != "" {
		return names, nil
	}
	subcommands := findProfileSubcommands(jirix)
	s := jirix.NewSeq()
	for _, sc := range subcommands {
		var out bytes.Buffer
		args := []string{"available"}
		if err := s.Capture(&out, nil).Last(sc, args...); err != nil {
			fmt.Fprintf(jirix.Stderr(), "failed to run %s %s: %v", sc, strings.Join(args, " "), err)
			return nil, err
		}
		mgrs := out.String()
		for _, m := range strings.Split(mgrs, ",") {
			names = append(names, strings.TrimSpace(m))
		}
	}
	return names, nil
}

func availableProfileManagers(jirix *jiri.X, dbpath string, args []string) ([]profileManager, *profiles.DB, error) {
	db := profiles.NewDB()
	if err := db.Read(jirix, dbpath); err != nil {
		fmt.Fprintf(jirix.Stderr(), "Failed to read profiles database %q: %v\n", dbpath, err)
		return nil, nil, err
	}
	mgrs := []profileManager{}
	names := args
	if len(names) == 0 {
		var err error
		names, err = allAvailableManagers(jirix)
		if err != nil {
			return nil, nil, err
		}
	}
	for _, name := range names {
		mgrs = append(mgrs, newProfileManager(name, db))
	}
	return mgrs, db, nil
}

func installedProfileManagers(jirix *jiri.X, dbpath string, args []string) ([]profileManager, *profiles.DB, error) {
	db := profiles.NewDB()
	if err := db.Read(jirix, dbpath); err != nil {
		fmt.Fprintf(jirix.Stderr(), "Failed to read profiles database %q: %v\n", dbpath, err)
		return nil, nil, err
	}
	mgrs := []profileManager{}
	names := args
	if len(names) == 0 {
		names = db.Names()
	}
	for _, name := range names {
		mgrs = append(mgrs, newProfileManager(name, db))
	}
	return mgrs, db, nil
}

func targetAtDefaultVersion(mgr profiles.Manager, target profiles.Target) (profiles.Target, error) {
	def := target
	version, err := mgr.VersionInfo().Select(target.Version())
	if err != nil {
		return profiles.Target{}, err
	}
	def.SetVersion(version)
	return def, nil
}

func writeDB(jirix *jiri.X, db *profiles.DB, installer, path string) error {

	if installer == "" {
		return nil
	}
	fi, err := os.Stat(path)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}

		if err := os.MkdirAll(path, os.FileMode(0755)); err != nil {
			return err
		}
	} else {
		if !fi.IsDir() {
			return fmt.Errorf("%s exists but is not a directory", path)
		}
	}

	return db.Write(jirix, installer, path)
}

func updateImpl(jirix *jiri.X, cl *updateFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.update(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func cleanupImpl(jirix *jiri.X, cl *cleanupFlagValues, args []string) error {
	count := 0
	if cl.gc {
		count++
	}
	if cl.rewriteDB {
		count++
	}
	if cl.rmAll {
		count++
	}
	if count != 1 {
		fmt.Errorf("exactly one option must be specified")
	}
	mgrs, db, err := installedProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.cleanup(jirix, cl, root); err != nil {
			return err
		}
	}
	if !cl.rmAll {
		return writeDB(jirix, db, profileInstaller, cl.dbPath)
	}
	return nil
}

func packagesImpl(jirix *jiri.X, cl *packagesFlagValues, args []string) error {
	mgrs, _, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	cl.target.UseCommandLineEnv()
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	s := jirix.NewSeq()
	installPackages := cl.installPackages

	cl.installPackages = false
	for _, mgr := range mgrs {
		cmds, err := mgr.packageCmds(jirix, cl, root)
		if err != nil {
			return err
		}
		for _, cmd := range cmds {
			if installPackages {
				if err := s.Verbose(true).Last(cmd[0], cmd[1:]...); err != nil {
					return err
				}
			} else {
				fmt.Fprintf(jirix.Stdout(), "%s\n", strings.TrimSpace(strings.Join(cmd, " ")))
			}
		}
	}
	return nil
}

func installImpl(jirix *jiri.X, cl *installFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	cl.target.UseCommandLineEnv()
	newMgrs := []profileManager{}
	for _, mgr := range mgrs {
		name := mgr.mgrName()
		if !cl.force {
			installer, profile := profiles.SplitProfileName(name)
			if p := db.LookupProfileTarget(installer, profile, cl.target); p != nil {
				fmt.Fprintf(jirix.Stdout(), "%v %v is already installed as %v\n", name, cl.target, p)
				continue
			}
		}
		newMgrs = append(newMgrs, mgr)
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range newMgrs {
		if err := mgr.install(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func uninstallImpl(jirix *jiri.X, cl *uninstallFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	if cl.allTargets && cl.target.IsSet() {
		fmt.Fprintf(jirix.Stdout(), "ignore target (%v) when used in conjunction with --all-targets\n", cl.target)
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.uninstall(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func availableImpl(jirix *jiri.X, cl *availableFlagValues, _ []string) error {
	if profileInstaller == "" {
		subcommands := findProfileSubcommands(jirix)
		if cl.verbose {
			fmt.Fprintf(jirix.Stdout(), "Available Subcommands: %s\n", strings.Join(subcommands, ", "))
		}
		s := jirix.NewSeq()
		args := []string{"available"}
		args = append(args, cl.args()...)
		out := bytes.Buffer{}
		for _, sc := range subcommands {
			if err := s.Capture(&out, nil).Last(sc, args...); err != nil {
				return err
			}
		}
		if s := strings.TrimSpace(out.String()); s != "" {
			fmt.Fprintln(jirix.Stdout(), s)
		}
	}
	mgrs := profilesmanager.Managers()
	if len(mgrs) == 0 {
		return nil
	}
	if cl.verbose {
		scname := ""
		if profileInstaller != "" {
			scname = profileInstaller + ": "
		}
		fmt.Fprintf(jirix.Stdout(), "%sAvailable Profiles:\n", scname)
		for _, name := range mgrs {
			mgr := profilesmanager.LookupManager(name)
			vi := mgr.VersionInfo()
			fmt.Fprintf(jirix.Stdout(), "%s: versions: %s\n", name, vi)
		}
	} else {
		if cl.describe {
			for _, name := range mgrs {
				mgr := profilesmanager.LookupManager(name)
				fmt.Fprintf(jirix.Stdout(), "%s: %s\n", name, strings.Replace(strings.TrimSpace(mgr.Info()), "\n", " ", -1))
			}
		} else {
			fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(mgrs, ", "))
		}
	}
	return nil
}
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package profilescmdline provides a command line driver
// (for v.io/x/lib/cmdline) for implementing jiri 'profile' subcommands.
// The intent is to support project specific instances of such profiles
// for managing software dependencies.
//
// There are two ways of using the cmdline support, one is to read profile
// information, via RegisterReaderCommands and
// RegisterReaderCommandsUsingParent; the other is to manage profile
// installations via the RegisterManagementCommands function. The management
// commands can manage profiles that are linked into the binary itself
// or invoke external commands that implement the profile management. These
// external 'installer' commands are accessed by specifing them as a prefix
// to the profile name. For example myproject::go will invoke the external
// command jiri-profile-myproject with "go" as the profile name. Thus the
// following invocations are equivalent:
// jiri profile install myproject::go
// jiri profile-myproject install go
//
// Regardless of which is used, the profile name, as seen by profile
// database readers will be myproject::go.
package profilescmdline

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

	"v.io/jiri"
	"v.io/jiri/profiles"
	"v.io/jiri/profiles/profilesmanager"
	"v.io/x/lib/cmdline"
	"v.io/x/lib/lookpath"
)

// newCmdOSPackages represents the "profile os-packages" command.
func newCmdOSPackages() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runPackages),
		Name:     "os-packages",
		Short:    "List the commands to install the OS packages required by the given profiles",
		Long:     "List or optionally run the commands to install the OS packages required by the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to list OS packages for.",
	}
}

// newCmdInstall represents the "profile install" command.
func newCmdInstall() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runInstall),
		Name:     "install",
		Short:    "Install the given profiles",
		Long:     "Install the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to install.",
	}
}

// newCmdUninstall represents the "profile uninstall" command.
func newCmdUninstall() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runUninstall),
		Name:     "uninstall",
		Short:    "Uninstall the given profiles",
		Long:     "Uninstall the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to uninstall.",
	}
}

// newCmdUpdate represents the "profile update" command.
func newCmdUpdate() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runUpdate),
		Name:     "update",
		Short:    "Install the latest default version of the given profiles",
		Long:     "Install the latest default version of the given profiles.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to update, if omitted all profiles are updated.",
	}
}

// newCmdCleanup represents the "profile cleanup" command.
func newCmdCleanup() *cmdline.Command {
	return &cmdline.Command{
		Runner:   jiri.RunnerFunc(runCleanup),
		Name:     "cleanup",
		Short:    "Cleanup the locally installed profiles",
		Long:     "Cleanup the locally installed profiles. This is generally required when recovering from earlier bugs or when preparing for a subsequent change to the profiles implementation.",
		ArgsName: "<profiles>",
		ArgsLong: "<profiles> is a list of profiles to cleanup, if omitted all profiles are cleaned.",
	}
}

// newCmdAvailable represents the "profile available" command.
func newCmdAvailable() *cmdline.Command {
	return &cmdline.Command{
		Runner: jiri.RunnerFunc(runAvailable),
		Name:   "available",
		Short:  "List the available profiles",
		Long:   "List the available profiles.",
	}
}

func runUpdate(jirix *jiri.X, args []string) error {
	return updateImpl(jirix, &updateFlags, args)
}

func runCleanup(jirix *jiri.X, args []string) error {
	return cleanupImpl(jirix, &cleanupFlags, args)
}

func runPackages(jirix *jiri.X, args []string) error {
	return packagesImpl(jirix, &packagesFlags, args)
}

func runInstall(jirix *jiri.X, args []string) error {
	return installImpl(jirix, &installFlags, args)
}

func runUninstall(jirix *jiri.X, args []string) error {
	return uninstallImpl(jirix, &uninstallFlags, args)
}

func runAvailable(jirix *jiri.X, args []string) error {
	return availableImpl(jirix, &availableFlags, args)
}

type commonFlagValues struct {
	// The value of --profiles-db
	dbPath string
	// The value of --profiles-dir
	root string
}

func initCommon(flags *flag.FlagSet, c *commonFlagValues, installer, defaultDBPath, defaultProfilesPath string) {
	RegisterDBPathFlag(flags, &c.dbPath, defaultDBPath)
	flags.StringVar(&c.root, "profiles-dir", defaultProfilesPath, "the directory, relative to JIRI_ROOT, that profiles are installed in")
}

func (cv *commonFlagValues) args() []string {
	a := append([]string{}, "--profiles-db="+cv.dbPath)
	a = append(a, "--profiles-dir="+cv.root)
	return a
}

type packagesFlagValues struct {
	commonFlagValues
	// The value of --target and --env
	target profiles.Target
	// Show commands for all required packages, rather than just the missing ones
	allPackages bool
	// Install the required packages
	installPackages bool
}

func initPackagesCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &packagesFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetAndEnvFlags(flags, &packagesFlags.target)
	flags.BoolVar(&packagesFlags.allPackages, "all", false, "print commands to install all required OS packages, not just those that are missing")
	flags.BoolVar(&packagesFlags.installPackages, "install", false, "install the requested packages. This may need to be run as root.")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Install)
	}
}

func (pv *packagesFlagValues) args() []string {
	a := pv.commonFlagValues.args()
	if t := pv.target.String(); t != "" {
		a = append(a, "--target="+t)
	}
	if e := pv.target.CommandLineEnv().String(); e != "" {
		a = append(a, "--target="+e)
	}
	a = append(a, fmt.Sprintf("--%s=%v", "all", pv.allPackages))
	return append(a, fmt.Sprintf("--%s=%v", "install", pv.installPackages))
}

type installFlagValues struct {
	commonFlagValues
	// The value of --target and --env
	target profiles.Target
	// The value of --force
	force bool
}

func initInstallCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &installFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetAndEnvFlags(flags, &installFlags.target)
	flags.BoolVar(&installFlags.force, "force", false, "force install the profile even if it is already installed")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Install)
	}
}

func (iv *installFlagValues) args() []string {
	a := iv.commonFlagValues.args()
	if t := iv.target.String(); t != "" {
		a = append(a, "--target="+t)
	}
	if e := iv.target.CommandLineEnv().String(); e != "" {
		a = append(a, "--target="+e)
	}
	return append(a, fmt.Sprintf("--%s=%v", "force", iv.force))
}

type uninstallFlagValues struct {
	commonFlagValues
	// The value of --target
	target profiles.Target
	// The value of --all-targets
	allTargets bool
	// The value of --v
	verbose bool
	// TODO(cnicolaou): add a flag to remove the profile only from the DB.
}

func initUninstallCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &uninstallFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	profiles.RegisterTargetFlag(flags, &uninstallFlags.target)
	flags.BoolVar(&uninstallFlags.allTargets, "all-targets", false, "apply to all targets for the specified profile(s)")
	flags.BoolVar(&uninstallFlags.verbose, "v", false, "print more detailed information")
	for _, name := range profilesmanager.Managers() {
		profilesmanager.LookupManager(name).AddFlags(flags, profiles.Uninstall)
	}
}

func (uv *uninstallFlagValues) args() []string {
	a := uv.commonFlagValues.args()
	if uv.target.String() != "" {
		a = append(a, "--target="+uv.target.String())
	}
	a = append(a, fmt.Sprintf("--%s=%v", "all-targets", uv.allTargets))
	return append(a, fmt.Sprintf("--%s=%v", "v", uv.verbose))
}

type cleanupFlagValues struct {
	commonFlagValues
	// The value of --gc
	gc bool
	// The value of --rewrite-profiles-db
	rewriteDB bool
	// The value of --rm-all
	rmAll bool
	// The value of --v
	verbose bool
}

func initCleanupCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &cleanupFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	flags.BoolVar(&cleanupFlags.gc, "gc", false, "uninstall profile targets that are older than the current default")
	flags.BoolVar(&cleanupFlags.rmAll, "rm-all", false, "remove profiles database and all profile generated output files.")
	flags.BoolVar(&cleanupFlags.rewriteDB, "rewrite-profiles-db", false, "rewrite the profiles database to use the latest schema version")
	flags.BoolVar(&cleanupFlags.verbose, "v", false, "print more detailed information")
}

func (cv *cleanupFlagValues) args() []string {
	return append(cv.commonFlagValues.args(),
		fmt.Sprintf("--%s=%v", "gc", cv.gc),
		fmt.Sprintf("--%s=%v", "rewrite-profiles-db", cv.rewriteDB),
		fmt.Sprintf("--%s=%v", "v", cv.verbose),
		fmt.Sprintf("--%s=%v", "rm-all", cv.rmAll))
}

type updateFlagValues struct {
	commonFlagValues
	// The value of --v
	verbose bool
}

func initUpdateCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	initCommon(flags, &updateFlags.commonFlagValues, installer, defaultDBPath, defaultProfilesPath)
	flags.BoolVar(&updateFlags.verbose, "v", false, "print more detailed information")
}

func (uv *updateFlagValues) args() []string {
	return append(uv.commonFlagValues.args(), fmt.Sprintf("--%s=%v", "v", uv.verbose))
}

type availableFlagValues struct {
	// The value of --v
	verbose bool
	// The value of --describe
	describe bool
}

func initAvailableCommand(flags *flag.FlagSet, installer, defaultDBPath, defaultProfilesPath string) {
	flags.BoolVar(&availableFlags.verbose, "v", false, "print more detailed information")
	flags.BoolVar(&availableFlags.describe, "describe", false, "print the profile description")
}

func (av *availableFlagValues) args() []string {
	return []string{
		fmt.Sprintf("--%s=%v", "v", av.verbose),
		fmt.Sprintf("--%s=%v", "describe", av.describe),
	}
}

var (
	packagesFlags    packagesFlagValues
	installFlags     installFlagValues
	uninstallFlags   uninstallFlagValues
	cleanupFlags     cleanupFlagValues
	updateFlags      updateFlagValues
	availableFlags   availableFlagValues
	profileInstaller string
	runSubcommands   bool
)

// RegisterManagementCommands registers the management subcommands:
// uninstall, install, update and cleanup.
func RegisterManagementCommands(parent *cmdline.Command, useSubcommands bool, installer, defaultDBPath, defaultProfilesPath string) {
	cmdOSPackages := newCmdOSPackages()
	cmdInstall := newCmdInstall()
	cmdUninstall := newCmdUninstall()
	cmdUpdate := newCmdUpdate()
	cmdCleanup := newCmdCleanup()
	cmdAvailable := newCmdAvailable()
	initPackagesCommand(&cmdOSPackages.Flags, installer, defaultDBPath, defaultProfilesPath)
	initInstallCommand(&cmdInstall.Flags, installer, defaultDBPath, defaultProfilesPath)
	initUninstallCommand(&cmdUninstall.Flags, installer, defaultDBPath, defaultProfilesPath)
	initUpdateCommand(&cmdUpdate.Flags, installer, defaultDBPath, defaultProfilesPath)
	initCleanupCommand(&cmdCleanup.Flags, installer, defaultDBPath, defaultProfilesPath)
	initAvailableCommand(&cmdAvailable.Flags, installer, defaultDBPath, defaultProfilesPath)
	parent.Children = append(parent.Children, cmdInstall, cmdOSPackages, cmdUninstall, cmdUpdate, cmdCleanup, cmdAvailable)
	profileInstaller = installer
	runSubcommands = useSubcommands
}

func findProfileSubcommands(jirix *jiri.X) []string {
	if !runSubcommands {
		return nil
	}
	cmds, _ := lookpath.LookPrefix(jirix.Env(), "jiri-profile-", nil)
	return cmds
}

func allAvailableManagers(jirix *jiri.X) ([]string, error) {
	names := profilesmanager.Managers()
	if profileInstaller != "" {
		return names, nil
	}
	subcommands := findProfileSubcommands(jirix)
	s := jirix.NewSeq()
	for _, sc := range subcommands {
		var out bytes.Buffer
		args := []string{"available"}
		if err := s.Capture(&out, nil).Last(sc, args...); err != nil {
			fmt.Fprintf(jirix.Stderr(), "failed to run %s %s: %v", sc, strings.Join(args, " "), err)
			return nil, err
		}
		mgrs := out.String()
		for _, m := range strings.Split(mgrs, ",") {
			names = append(names, strings.TrimSpace(m))
		}
	}
	return names, nil
}

// availableProfileManagers creates a profileManager for all available
// profiles, whether in this process or in a sub command.
func availableProfileManagers(jirix *jiri.X, dbpath string, args []string) ([]profileManager, *profiles.DB, error) {
	db := profiles.NewDB()
	if err := db.Read(jirix, dbpath); err != nil {
		fmt.Fprintf(jirix.Stderr(), "Failed to read profiles database %q: %v\n", dbpath, err)
		return nil, nil, err
	}
	mgrs := []profileManager{}
	names := args
	if len(names) == 0 {
		var err error
		names, err = allAvailableManagers(jirix)
		if err != nil {
			return nil, nil, err
		}
	}
	for _, name := range names {
		mgrs = append(mgrs, newProfileManager(name, db))
	}
	return mgrs, db, nil
}

// installedProfileManagers creates a profileManager for all installed
// profiles, whether in this process or in a sub command.
func installedProfileManagers(jirix *jiri.X, dbpath string, args []string) ([]profileManager, *profiles.DB, error) {
	db := profiles.NewDB()
	if err := db.Read(jirix, dbpath); err != nil {
		fmt.Fprintf(jirix.Stderr(), "Failed to read profiles database %q: %v\n", dbpath, err)
		return nil, nil, err
	}
	mgrs := []profileManager{}
	names := args
	if len(names) == 0 {
		names = db.Names()
	}
	for _, name := range names {
		mgrs = append(mgrs, newProfileManager(name, db))
	}
	return mgrs, db, nil
}

func targetAtDefaultVersion(mgr profiles.Manager, target profiles.Target) (profiles.Target, error) {
	def := target
	version, err := mgr.VersionInfo().Select(target.Version())
	if err != nil {
		return profiles.Target{}, err
	}
	def.SetVersion(version)
	return def, nil
}

func writeDB(jirix *jiri.X, db *profiles.DB, installer, path string) error {
	// Do nothing if the installer is not supplied. This will generally
	// happen when/if writeDB is called from the top-level profile driver
	// command rather than from a subcommand.
	if installer == "" {
		return nil
	}
	fi, err := os.Stat(path)
	if err != nil {
		if !os.IsNotExist(err) {
			return err
		}
		// New setup, but the directory doesn't exist yet.
		if err := os.MkdirAll(path, os.FileMode(0755)); err != nil {
			return err
		}
	} else {
		if !fi.IsDir() {
			return fmt.Errorf("%s exists but is not a directory", path)
		}
	}
	// New setup with installers writing their own file in a directory
	return db.Write(jirix, installer, path)
}

func updateImpl(jirix *jiri.X, cl *updateFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.update(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func cleanupImpl(jirix *jiri.X, cl *cleanupFlagValues, args []string) error {
	count := 0
	if cl.gc {
		count++
	}
	if cl.rewriteDB {
		count++
	}
	if cl.rmAll {
		count++
	}
	if count != 1 {
		fmt.Errorf("exactly one option must be specified")
	}
	mgrs, db, err := installedProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.cleanup(jirix, cl, root); err != nil {
			return err
		}
	}
	if !cl.rmAll {
		return writeDB(jirix, db, profileInstaller, cl.dbPath)
	}
	return nil
}

func packagesImpl(jirix *jiri.X, cl *packagesFlagValues, args []string) error {
	mgrs, _, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	cl.target.UseCommandLineEnv()
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	s := jirix.NewSeq()
	installPackages := cl.installPackages
	// Never ask a subcommand to install packages.
	cl.installPackages = false
	for _, mgr := range mgrs {
		cmds, err := mgr.packageCmds(jirix, cl, root)
		if err != nil {
			return err
		}
		for _, cmd := range cmds {
			if installPackages {
				if err := s.Verbose(true).Last(cmd[0], cmd[1:]...); err != nil {
					return err
				}
			} else {
				fmt.Fprintf(jirix.Stdout(), "%s\n", strings.TrimSpace(strings.Join(cmd, " ")))
			}
		}
	}
	return nil
}

func installImpl(jirix *jiri.X, cl *installFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	cl.target.UseCommandLineEnv()
	newMgrs := []profileManager{}
	for _, mgr := range mgrs {
		name := mgr.mgrName()
		if !cl.force {
			installer, profile := profiles.SplitProfileName(name)
			if p := db.LookupProfileTarget(installer, profile, cl.target); p != nil {
				fmt.Fprintf(jirix.Stdout(), "%v %v is already installed as %v\n", name, cl.target, p)
				continue
			}
		}
		newMgrs = append(newMgrs, mgr)
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range newMgrs {
		if err := mgr.install(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func uninstallImpl(jirix *jiri.X, cl *uninstallFlagValues, args []string) error {
	mgrs, db, err := availableProfileManagers(jirix, cl.dbPath, args)
	if err != nil {
		return err
	}
	if cl.allTargets && cl.target.IsSet() {
		fmt.Fprintf(jirix.Stdout(), "ignore target (%v) when used in conjunction with --all-targets\n", cl.target)
	}
	root := jiri.NewRelPath(cl.root).Join(profileInstaller)
	for _, mgr := range mgrs {
		if err := mgr.uninstall(jirix, cl, root); err != nil {
			return err
		}
	}
	return writeDB(jirix, db, profileInstaller, cl.dbPath)
}

func availableImpl(jirix *jiri.X, cl *availableFlagValues, _ []string) error {
	if profileInstaller == "" {
		subcommands := findProfileSubcommands(jirix)
		if cl.verbose {
			fmt.Fprintf(jirix.Stdout(), "Available Subcommands: %s\n", strings.Join(subcommands, ", "))
		}
		s := jirix.NewSeq()
		args := []string{"available"}
		args = append(args, cl.args()...)
		out := bytes.Buffer{}
		for _, sc := range subcommands {
			if err := s.Capture(&out, nil).Last(sc, args...); err != nil {
				return err
			}
		}
		if s := strings.TrimSpace(out.String()); s != "" {
			fmt.Fprintln(jirix.Stdout(), s)
		}
	}
	mgrs := profilesmanager.Managers()
	if len(mgrs) == 0 {
		return nil
	}
	if cl.verbose {
		scname := ""
		if profileInstaller != "" {
			scname = profileInstaller + ": "
		}
		fmt.Fprintf(jirix.Stdout(), "%sAvailable Profiles:\n", scname)
		for _, name := range mgrs {
			mgr := profilesmanager.LookupManager(name)
			vi := mgr.VersionInfo()
			fmt.Fprintf(jirix.Stdout(), "%s: versions: %s\n", name, vi)
		}
	} else {
		if cl.describe {
			for _, name := range mgrs {
				mgr := profilesmanager.LookupManager(name)
				fmt.Fprintf(jirix.Stdout(), "%s: %s\n", name, strings.Replace(strings.TrimSpace(mgr.Info()), "\n", " ", -1))
			}
		} else {
			fmt.Fprintf(jirix.Stdout(), "%s\n", strings.Join(mgrs, ", "))
		}
	}
	return nil
}
