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) {
	var ( // Set up everything that does io
		stdout   = bufio.NewWriter(args.Ostream)
		executor = cli.Executor{ // To run external commands
			Stdout: args.Ostream,
			Stderr: args.Estream,
		}
		pager    = cli.Pager(&executor, args.FallbackPager)    // To display files
		settings = newSettings(args.Argv, args.DefaultPkgFile) // Parse command line arguments
	)
	defer stdout.Flush()
	log.SetOutput(args.Estream) // Log to stderr

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

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

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

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

	switch settings.Cmd {

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

	case viewBuild:
		pMap.IterOver(settings.PkgNames,
			func(pkg *sborepo.Package) { pager(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.GetPkg(pName).Description())
				}
			}
		}
		sort.Strings(matches)
		stdout.WriteString(strings.Join(matches, "\n") + "\n")

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

	case tracking:
		dg := pMap.DependencyGraph(settings.PkgNames)
		dg.ProcessTopological(func(pkg *sborepo.PkgNode) { pkg.Write(stdout) })

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

	case upgrade:
		var dg sborepo.DependencyGraph
		if settings.NoDeps { // Upgrade only the packages in the list
			dg = pMap.NamedPackageGraph(settings.PkgNames)
		} else { // Upgrade the packages in the list and their dependencies
			dg = pMap.DependencyGraph(settings.PkgNames)
		}

		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.PanicOnFail(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); len(FilesToDownload) > 0 { // Download
			executor.PanicOnFail(args.DownloadCmd + " " + strings.Join(FilesToDownload, " "))
			noBuildError = checkDownloads(dg, args.TmpDir, 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 {
				logPkgCmdPanicOnFail(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)
			logPkgCmdPanicOnFail(pkg, cmd, "Install", progress)

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

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