package pkg

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

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

// Parameters to allow injection of normally constant values, so that some of
// Run's behavior can be controlled without changing the source code.
type RunArgs struct {
	// It is ensured that all i/o operations are done through Ostream, Istream
	// and Estream.
	Ostream io.Writer
	Istream io.Reader
	Estream io.Writer

	// Argv holds the command line arguments, check pkg's user documentation
	// for more information about them.
	Argv []string

	// Check the documentation of the similarly named constants for more
	// information about them.
	RepoPath, DocDir, TmpDir, ConfigDir,
	Greylist, Blacklist, OtherBlacklist, DefaultPkgFile, SlackwareVersionFile,
	PullCmd, SyncCmd, CleanCmd, DownloadCmd, InstallCmd,
	FallbackPager string
}

// 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.Ostream)
	defer stdout.Flush()
	log.SetOutput(args.Estream) // Log to stderr

	settings, err := newSettings(args.Argv, args.DefaultPkgFile) // Parse command line arguments
	if err != nil {
		log.Fatal(err)
	}

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

	executor := cli.Executor{ // To run external commands
		Stdout: args.Ostream,
		Stderr: args.Estream,
	}
	pager := cli.Pager(&executor, args.FallbackPager) // To display files

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

	if settings.Update {
		executor.MustRun(args.SyncCmd)
	}

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

	DependencyGraph := func() sborepo.DependencyGraph {
		var newDg func([]string) (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():
			log.Fatal(err)
		case settings.Yes:
		case !cli.PromptUser(stdout, args.Istream, err.Error()+".\nContinue?"):
			log.Fatal("Aborting.")
		}
		return dg
	}

	switch settings.Cmd {

	case view:
		if err := pMap.IterOver(settings.PkgNames, func(pkg *sborepo.Package) {
			pager(pkg.AllReadmes(), pkg.InfoFile())
		}); err != nil {
			log.Fatal(err)
		}

	case viewBuild:
		if err := pMap.IterOver(settings.PkgNames, func(pkg *sborepo.Package) {
			pager(pkg.SlackBuild())
		}); err != nil {
			log.Fatal(err)
		}

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

	case dependents:
		if rootPkg, ok := DependencyGraph().GetPkgNode(settings.Root); ok {
			rootPkg.ProcessDependents(func(pkg *sborepo.PkgNode) { pkg.Write(stdout) })
		}

	case tracking:
		DependencyGraph().ProcessTopological(func(pkg *sborepo.PkgNode) { pkg.Write(stdout) })

	case listInstalled:
		for _, file := range must.ReadDir(args.DocDir) {
			pName := sborepo.PackageNameFromFullName(file.Name())
			if _, ok := pMap[pName]; ok {
				stdout.WriteString(pName + "\n")
			}
		}

	case upgrade:
		dg := DependencyGraph()

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

		if settings.FilterInstalled { // Remove installed packages from the graph
			dg.FilterInstalledPkgs(args.DocDir, sborepo.KernelVersion())
		}

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

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

		if !settings.Yes { // Let user exclude packages and read README files if required
			letUserExcludePkgs(dg, stdout, args.Istream)
			dg.ProcessUnordered(func(pkg *sborepo.PkgNode) {
				promptIfReadmeRequired(pkg, stdout, args.Istream, pager)
			})
			if dg.IsEmpty() {
				stdout.WriteString("All packages skipped\n")
				break
			}
		}

		outputDir := must.SetEnvIfNotSet("OUTPUT", args.TmpDir)
		must.Mkdir(outputDir)
		executor.MustRun(args.CleanCmd)

		var pkgNames []string
		dg.ProcessUnordered(func(pkg *sborepo.PkgNode) {
			pkgNames = append(pkgNames, pkg.Name)
		})
		progress := cli.ProgressReporter(pkgNames, args.Estream)
		noBuildError := true

		if FilesToDownload := filesToDownload(dg, args.TmpDir, settings.CheckMD5Sum); len(FilesToDownload) > 0 { // Download
			executor.MustRun(args.DownloadCmd + " " + strings.Join(FilesToDownload, " "))
			noBuildError = checkDownloads(dg, args.TmpDir, settings.CheckMD5Sum, progress) // Check if all files were downloaded
		}

		buildSlots := make(chan struct{}, settings.Jobs) // limit concurrent builds

		dg.ProcessTopological(func(pkg *sborepo.PkgNode) { // Build and install
			for _, DownloadName := range pkg.DownloadNames() {
				must.Symlink(
					filepath.Join(args.TmpDir, DownloadName),
					filepath.Join(pkg.Path(), DownloadName),
				)
			}

			// Execute pre-build script if available
			if cmd, err := preBuildCmd(pkg, args.ConfigDir); err == nil {
				logPkgCmdMustRun(pkg, cmd, "Prebuild", progress)
			}

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

			// Install
			cmd = args.InstallCmd + pkg.FindBuildPkg(outputDir)
			logPkgCmdMustRun(pkg, cmd, "Install", progress)

			progress(pkg.Name, "installed")
		})

		if noBuildError { // Clean up if there were no build errors
			executor.MustRun(args.CleanCmd)
			stdout.WriteString("All packages installed successfully\n")
		}
	}
}
