package pkg

import (
	"bufio"
	"log"
	"path/filepath"
	"sort"
	"strings"

	"gitlab.com/M0M097/pkg/cli"
	"gitlab.com/M0M097/pkg/internal/filecheck"
	"gitlab.com/M0M097/pkg/internal/must"
	"gitlab.com/M0M097/pkg/sborepo"
)

// RunArgs groups the command-line arguments, configuration, and streams used by
// Run and RunWithUI.
type RunArgs struct {
	// Argv contains the command-line arguments. Check pkg's user documentation
	// for more information about them.
	Argv []string

	Config  Config  // quasi-constant values (file paths, commands, etc.)
	Streams Streams // All i/o operations are done via these streams
}

// Run is the high-level, real main function of the pkg tool.
// It is a dedicated function to ease testing and maybe embedding in other
// programs.
func Run(args RunArgs) {
	stdout := bufio.NewWriter(args.Streams.Out)
	defer stdout.Flush()
	var (
		executor = cli.Executor{
			Stdout: args.Streams.Out,
			Stderr: args.Streams.Err,
			Log:    true,
		}
		ui = CliUI{
			stdout: stdout,
			stdin:  args.Streams.In,
			stderr: args.Streams.Err,
			pager:  cli.Pager(executor, args.Config.FallbackPager), // To display files
		}
		settings, err = NewSettings(args.Argv, args.Config.DefaultPkgFile) // Parse cli args
	)
	if err != nil {
		ui.Fatal(err)
	}
	log.SetOutput(args.Streams.Err) // Log to stderr
	RunWithUI(args.Config, &ui, settings, executor)
}

// RunWithUI executes the workflow described by settings using the supplied UI
// and command executor.
//
// Settings.Cmd selects the top-level workflow. NewSettings can be used to build
// a coherent Settings value from command-line arguments.
func RunWithUI(cfg Config, ui UI, settings Settings, executor cli.Executor) {

	if settings.Cmd == CommandShowVersion {
		ui.Alert(VERSION)
		return // If only version is requested, no need to do anything else
	}

	if !filecheck.Exists(cfg.RepoPath) { // Make sure the repository exists
		must.Mkdir(filepath.Dir(cfg.RepoPath))
		must.Must(executor.Run(buildPullCmd(cfg.PullCmd, cfg.SlackwareVersionFile)))
	}

	if settings.Update {
		must.Must(executor.Run(cfg.SyncCmd))
	}

	pMap := sborepo.NewRepoMap(cfg.RepoPath) // Read all packages from the repository

	dependencyGraph := func() sborepo.DependencyGraph {
		var newDg func([]sborepo.PkgName) (sborepo.DependencyGraph, error)
		if settings.NoDeps { // Include only the packages in the list.
			newDg = pMap.NamedPackageGraph
		} else { // Include the packages in the list and their dependencies.
			newDg = pMap.DependencyGraph
		}
		dg, err := newDg(settings.PkgNames)
		switch {
		case err == nil:
		case dg.IsEmpty():
			ui.Fatal(err)
		case settings.Yes:
		case !ui.Prompt(err.Error() + ".\nContinue?"):
			ui.Fatal("Aborting.")
		}
		return dg
	}

	display := func(fs ...func(pkg *sborepo.Package) string) {
		for _, f := range fs {
			iter, err := pMap.IterOver(settings.PkgNames)
			if err != nil {
				ui.Fatal(err)
			}
			iter(func(pkg *sborepo.Package) { must.Must(ui.Display(f(pkg))) })
		}
	}

	listLines := func(iter sborepo.PkgIter) { ui.ListLines(sborepo.Names(iter)) }

	switch settings.Cmd {

	case CommandView:
		display((*sborepo.Package).AllReadmes, (*sborepo.Package).InfoFile)

	case CommandViewBuild:
		display((*sborepo.Package).SlackBuild)

	case CommandDependents:
		listLines(dependencyGraph().IterDependents(settings.Root))

	case CommandTracking:
		listLines(dependencyGraph().ProcessTopological)

	case CommandListInstalled:
		listLines(pMap.Installed(cfg.DocDir))

	case CommandSearch:
		var matches []string
		for _, searchString := range settings.PkgNames {
			iter, err := pMap.Search(searchString)
			if err != nil {
				ui.Fatal(err)
			}
			iter(func(pkg *sborepo.Package) { matches = append(matches, pkg.Description()) })
		}
		sort.Strings(matches)
		ui.ListLines(matches)

	case CommandUpgrade:
		dg := dependencyGraph()

		if settings.UpdateBlacklist {
			err := updateBlacklist(dg, cfg.Blacklist, cfg.OtherBlacklist, cfg.DefaultPkgFile)
			if err != nil {
				log.Println(cli.Banner("Skipping blacklist update", err.Error(), OUTPUT_WIDTH))
			} else {
				log.Println("Blacklist updated")
			}
		}

		if settings.FilterInstalled { // Remove installed packages from the graph
			kernelVersion := sborepo.KernelVersion()
			dg.FilterOut(func(pkg *sborepo.Package) bool {
				return pkg.IsUpToDate(cfg.DocDir, kernelVersion)
			})
		}

		if settings.RespectGreylist { // Remove greylisted packages from the graph
			if greylisted := pkgNameFromFile(cfg.Greylist); len(greylisted) > 0 {
				dg.Remove(greylisted...)
				log.Printf("greylisted: %v\n", greylisted)
			}
		}

		if dg.IsEmpty() {
			ui.Alert("Nothing to do")
			break
		}

		if !settings.Yes { // Let user exclude packages and read README files if required
			ui.ChoosePkgs(dg.ProcessUnordered)(func(pkg *sborepo.Package) { dg.Remove(pkg.Name) })
			dg.ProcessUnordered(func(pkg *sborepo.Package) { promptIfReadmeRequired(pkg, dg, ui) })
			if dg.IsEmpty() {
				ui.Alert("All packages skipped")
				break
			}
		}

		outputDir := must.SetEnvIfNotSet("OUTPUT", cfg.TmpDir)
		must.Mkdir(outputDir)
		must.Must(executor.Run(cfg.CleanCmd))

		var (
			progress     = ui.NewProgressReporter(sborepo.Names(dg.ProcessUnordered))
			noBuildError = true
		)
		if downloads := filesToDownload(dg, cfg.TmpDir, settings.CheckMD5Sum); len(downloads) > 0 { // Download
			must.Must(executor.Run(cfg.DownloadCmd + " " + strings.Join(downloads, " ")))
			noBuildError = checkDownloads(dg, cfg.TmpDir, settings.CheckMD5Sum, progress) // Check if all files were downloaded
		}

		buildSlots := make(chan struct{}, settings.Jobs)   // limit concurrent builds
		dg.ProcessTopological(func(pkg *sborepo.Package) { // Build and install
			for _, downloadName := range pkg.DownloadNames() {
				must.Symlink(
					filepath.Join(cfg.TmpDir, downloadName),
					filepath.Join(pkg.Path(), downloadName),
				)
			}

			// Execute pre-build script if available
			if cmd, err := preBuildCmd(pkg, cfg.ConfigDir); err == nil {
				must.Must(logPkgCmd(pkg, dg, cmd, "Prebuild", progress))
			}

			// Build
			cmd := buildCmd(pkg, settings.BuildOptions[pkg.Name])
			buildSlots <- struct{}{}
			ok := logPkgCmd(pkg, dg, cmd, "Build", progress)
			<-buildSlots
			if ok != nil {
				noBuildError = false
				return
			}

			// Install
			cmd = cfg.InstallCmd + pkg.FindBuildPkg(outputDir)
			must.Must(logPkgCmd(pkg, dg, cmd, "Install", progress))
			progress(pkg.Name, "installed")
		})

		if noBuildError { // Clean up if there were no build errors
			must.Must(executor.Run(cfg.CleanCmd))
			ui.Alert("All packages installed successfully")
		}
	}
}
