package pkg

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

	"gitlab.com/M0M097/pkg/lib/sboRepo"
	"gitlab.com/M0M097/pkg/lib/utils"
)

// 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.
//
// It is ensured that all i/o operations are done through ostream, istream
// and estream.
//
// argv holds the command line arguments, check pkg's user documentation
// for more information about them.
//
// The other parameters allow injection of normally constant values, so
// that some of Run's behavior can be controlled without changing the source
// code. Check the documentation of the similarly named constants for more
// information about them.
func Run(
	ostream io.Writer, istream io.Reader, estream io.Writer, // i/o streams
	argv []string, // Command line arguments
	RepoPath, DocDir, TmpDir, ConfigDir, // Constant directory paths
	Greylist, Blacklist, OtherBlacklist, DefaultPkgFile, // Constant filepaths
	PullCmd, SyncCmd, CleanCmd, DownloadCmd, InstallCmd, // Commands
	FallbackPager string,
) {
	var ( // Set up everything that does io
		stdout   = bufio.NewWriter(ostream)
		logger   = log.Default()
		executor = utils.Executor{ // To run external commands
			Stdout: ostream,
			Stderr: estream,
			Logger: logger,
		}
		pager    = utils.NewPager(&executor, FallbackPager) // To display files
		settings = newSettings(argv, DefaultPkgFile)        // Parse command line arguments
	)
	defer stdout.Flush()
	logger.SetOutput(stdout)

	if settings.Cmd() == showVersion {
		stdout.WriteString(VERSION + "\n")
		return // If only version is requested, no need to do anything else
	}

	if !utils.Exists(RepoPath) { // Make sure the repository exists
		utils.Mkdir(filepath.Dir(RepoPath))
		executor.PanicOnFail(PullCmd)
	}

	if settings.Update() {
		executor.PanicOnFail(SyncCmd)
	}

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

	switch settings.Cmd() {

	case view:
		pMap.IterOver(settings.PkgNames(),
			func(pkg *sboRepo.Package) { pager.Display(pkg.AllReadmes(), pkg.InfoFile()) })

	case viewBuild:
		pMap.IterOver(settings.PkgNames(),
			func(pkg *sboRepo.Package) { pager.Display(pkg.SlackBuild()) })

	case search:
		var matches []string
		for _, searchString := range settings.PkgNames() {
			re := regexp.MustCompile(searchString)
			for pName := range pMap {
				if re.MatchString(pName) {
					matches = append(matches, pMap.Get(pName).Description())
				}
			}
		}
		sort.Strings(matches)
		stdout.WriteString(strings.Join(matches, "\n") + "\n")

	case dependees:
		pMap.ProcessList(settings.PkgNames())
		pMap.Get(settings.Root()).IterateDependees(
			func(pkg *sboRepo.Package) { pkg.Write(stdout) })

	case tracking:
		pMap.ProcessList(settings.PkgNames()).Iterate(
			func(pkg *sboRepo.Package) { pkg.Write(stdout) })

	case listInstalled:
		for _, file := range utils.ReadDir(DocDir) {
			if pName := fullPkgName2PkgName(file.Name()); pMap.Contains(pName) {
				stdout.WriteString(pName + "\n")
			}
		}

	case upgrade:
		var (
			pErr  packageErrors
			pList sboRepo.Head
		)

		if settings.NoDeps() { // Upgrade only the packages in the list
			pList = pMap.PkgNames2List(settings.PkgNames())
		} else { // Upgrade the packages in the list and their dependencies
			pList = pMap.ProcessList(settings.PkgNames())
		}

		// Call after creation of pList since if this panics the handler would write
		// a misleading success message
		defer pErr.final(stdout)

		if settings.UpdateBlacklist() {
			if err := updateBlacklist(
				pList,
				Blacklist,
				OtherBlacklist,
				DefaultPkgFile,
			); err != nil {
				logger.Println(utils.Banner("Skipping blacklist update", err.Error()))
			} else {
				logger.Println("Blacklist updated")
			}
		}

		if settings.FilterInstalled() { // Remove installed packages from process list
			filterInstalledPkgs(pList, DocDir, utils.GetKernelVersion())
		}

		if settings.RespectGreylist() { // Remove greylisted packages from process list
			filterGreylistedPkgs(pMap, Greylist, &pErr)
		}

		if pList.IsEmpty() {
			stdout.WriteString("Nothing to do\n")
			break
		}

		if !settings.Yes() { // Let user exclude packages and read README files if required
			letUserExcludePkgs(pList, stdout, istream, &pErr)
			pList.Iterate(func(pkg *sboRepo.Package) {
				promptIfReadmeRequired(pkg, stdout, istream, pager, &pErr)
			})
			if pList.IsEmpty() {
				stdout.WriteString("All packages skipped\n")
				break
			}
		}

		utils.Mkdir(utils.SetEnvIfNotSet("OUTPUT", TmpDir)) // Output directory

		executor.PanicOnFail(CleanCmd)

		if FilesToDownload := filesToDownload(pList); len(FilesToDownload) > 0 { // Download
			executor.PanicOnFail(DownloadCmd + " " + utils.Join(FilesToDownload))
			pErr.checkDownloads(pList, TmpDir) // Check if all files were downloaded
		}

		pList.Iterate(func(pkg *sboRepo.Package) { // Build, and install the packages
			for _, DownloadName := range pkg.DownloadNames() {
				utils.Symlink(
					filepath.Join(TmpDir, DownloadName),
					filepath.Join(pkg.Path(), DownloadName),
				)
			}

			// Execute pre-build script if available
			if cmd, err := preBuildCmd(pkg, ConfigDir); err == nil {
				executor.PanicOnFail(cmd)
			}

			// Build
			if executor.Run(buildCmd(pkg, settings.BuildOptions()[pkg.Name()])) != nil {
				pErr.skipPkgAndDependees(pkg, "Failed to build")
				return
			}

			// Install
			executor.PanicOnFail(InstallCmd + pkg.FindBuildPkg(TmpDir))
		})

		executor.PanicOnFail(CleanCmd) // Clean up
	}
}
