package pkg

import (
	"errors"
	"log"
	"os"
	"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"
)

// UI defines the user-interaction operations used by RunWithUI.
type UI interface {
	// Alert displays a message.
	Alert(string)
	// ListLines displays lines of text.
	ListLines([]string)
	// ChoosePkgs prompts the user to choose packages from the provided iterator.
	ChoosePkgs(sborepo.PkgIter) sborepo.PkgIter
	// Display shows one or more files to the user.
	Display(...string) error
	// Prompt asks the user a yes/no question.
	Prompt(string) bool
	// NewProgressReporter creates a per-package progress reporter.
	NewProgressReporter(taskLabels []string) cli.ProgressReporter
	// Fatal reports a fatal error and must terminate the current run.
	Fatal(v ...interface{})
}

// 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 repo

	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 CommandCleanSystem:
		if err := prepareSlackpkgConfig(
			cfg.SlackpkgConfigDir, cfg.SlackpkgSystemConfigDir,
		); err != nil {
			ui.Fatal(err)
		}
		if err := updateBlacklist(
			dependencyGraph(), cfg.Blacklist, cfg.SlackpkgSystemBlacklist,
		); err != nil {
			ui.Fatal(err)
		}
		must.Must(executor.Run(cfg.SlackpkgCleanCmd))

	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.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
			greylisted := pkgNameFromFile(cfg.Greylist)
			if len(greylisted) > 0 {
				dg.Remove(greylisted...)
				log.Printf("greylisted: %v\n", greylisted)
			}
		}

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

		// Let user exclude packages and read README files if required
		if !settings.Yes {
			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
			}
		}

		if downloadBuildInstall(
			dg, cfg, settings,
			ui.NewProgressReporter(sborepo.Names(dg.ProcessUnordered)),
			executor,
		) == nil { // Clean up if there were no build errors
			must.Must(executor.Run(cfg.CleanCmd))
			ui.Alert("All packages installed successfully")
		}
	}
}

func downloadBuildInstall(
	dg sborepo.DependencyGraph,
	cfg Config,
	settings Settings,
	progress cli.ProgressReporter,
	executor cli.Executor,
) error {
	noBuildError := true
	outputDir := must.SetEnvIfNotSet("OUTPUT", cfg.TmpDir)
	must.Mkdir(outputDir)
	must.Must(executor.Run(cfg.CleanCmd))

	func() { // Download
		to_download := filesToDownload(dg, cfg.TmpDir, settings.CheckMD5Sum)
		if len(to_download) == 0 {
			return
		}
		cmd := cfg.DownloadCmd + " " + strings.Join(to_download, " ")
		must.Must(executor.Run(cmd))
		noBuildError = checkDownloads( // Check if all files were downloaded
			dg, cfg.TmpDir, settings.CheckMD5Sum, progress,
		)
	}()

	buildSlots := make(chan struct{}, settings.Jobs)   // limit concurrent builds
	dg.ProcessTopological(func(pkg *sborepo.Package) { // Build and install
		for _, downloadName := range pkg.DownloadNames() {
			must.Must(os.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 {
		return nil
	}
	return errors.New("build/download error(s) occurred and were reported.")
}
