package pkg

import (
	"bufio"
	"crypto/md5"
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"sync"

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

// FilterGreylistedPkgs removes packages listed in a file with the given path `greylist`
func filterGreylistedPkgs(dg sborepo.DependencyGraph, greylist string) []string {
	f, err := os.Open(greylist)
	if err != nil {
		return nil
	}
	defer f.Close()
	return dg.Remove(cli.StripCommentsAndTokenize(f))
}

// LetUserExcludePkgs prompts the user to exclude some packages from the list
func letUserExcludePkgs(dg sborepo.DependencyGraph, w *bufio.Writer, r io.Reader) {
	var (
		i        int
		pkgArray []*sborepo.PkgNode
	)
	// Ask user which packages to exclude
	dg.ProcessUnordered(func(pkg *sborepo.PkgNode) {
		pkgArray = append(pkgArray, pkg)
		i++
		fmt.Fprintln(w, i, pkg.Description())
	})
	w.WriteString("Space separated list of packages to exclude:\n")
	w.Flush()
	// Parse user input
	scanner := bufio.NewScanner(r)
	scanner.Scan()
	for _, field := range strings.Fields(scanner.Text()) {
		num, err := strconv.Atoi(field)
		if err != nil || num < 1 || num > i {
			fmt.Fprintf(w, "Invalid input: `%s` Repeat...\n", field)
			letUserExcludePkgs(dg, w, r)
			return
		}
		pkg := pkgArray[num-1]
		pkg.InGraph = false
	}
}

// UpdateBlacklist copies the otherBlacklist file to the blacklist file and
// appends the names of the packages in the list to the blacklist file.
func updateBlacklist(dg sborepo.DependencyGraph, blacklist, otherBlacklist, DefaultFile string) error {
	dest, err := os.Create(blacklist)
	if err != nil {
		return err
	}
	defer dest.Close()

	src, err := os.Open(otherBlacklist)
	if err != nil {
		return fmt.Errorf(`%w

%s will be copied to %s before the packages listed in %s and their dependencies are appended.

Either copy your existing %s to %s, or back it up and create an empty file %s.`,
			err,
			otherBlacklist, blacklist, DefaultFile,
			blacklist, otherBlacklist, otherBlacklist)
	}
	defer src.Close()

	must.Copy(src, dest)
	dg.ProcessUnordered(func(pkg *sborepo.PkgNode) { pkg.Write(dest) })
	return nil
}

// FilesToDownload returns a slice of URLs to download for package sources that
// are not already downloaded or have different MD5 sums.
func filesToDownload(dg sborepo.DependencyGraph, tmpDir string) []string {
	var (
		FilesToDownload []string
		mu              sync.Mutex
	)
	sborepo.Parallel(dg.ProcessUnordered)(func(pkg *sborepo.PkgNode) {
		var (
			md5Sums       = pkg.MD5Sums()
			urls          = pkg.DownloadURLs()
			DownloadNames = pkg.DownloadNames()
			wg            sync.WaitGroup
		)
		wg.Add(len(DownloadNames))
		for i, DownloadName := range DownloadNames {
			i, DownloadName := i, DownloadName
			checksum := ""
			if i < len(md5Sums) {
				checksum = md5Sums[i]
			}
			go func() {
				defer wg.Done()
				if match, _ := filecheck.HashMatches(
					filepath.Join(tmpDir, DownloadName), checksum, md5.New(),
				); !match {
					mu.Lock()
					FilesToDownload = append(FilesToDownload, urls[i])
					mu.Unlock()
				}
			}()
		}
		wg.Wait()
	})
	return FilesToDownload
}

// PromptIfReadmeRequired prompts the user to apply `pager` on the README file
// if it is listed as required in the info file.
func promptIfReadmeRequired(
	pkg *sborepo.PkgNode,
	w *bufio.Writer,
	r io.Reader,
	pager func(...string),
) {
	if pkg.IsReadmeRequired() && cli.PromptUser(w, r, pkg.Name+
		": Requires to read the README file. Do you want "+
		"to read it (and optionally skip the package) now?",
	) {
		pager(pkg.AllReadmes())
		deps := pkg.IndirectDependents()
		prompt := fmt.Sprintf("Do you want to skip " + pkg.Name)
		if len(deps) > 0 {
			depNames := make([]string, len(deps))
			for i, dep := range deps {
				depNames[i] = dep.Name
			}
			prompt += fmt.Sprintf(" and its dependents %v", depNames)
		}
		if cli.PromptUser(w, r, prompt) {
			pkg.InGraph = false
			for _, dep := range deps {
				dep.InGraph = false
			}
		}
	}
}

func buildCmd(pkg *sborepo.PkgNode, buildOptions []string) string {
	if len(buildOptions) == 0 {
		return BUILD_CMD + pkg.SlackBuild()
	}
	return ENV_CMD + strings.Join(buildOptions, " ") + " " + BUILD_CMD + pkg.SlackBuild()
}

func preBuildCmd(pkg *sborepo.PkgNode, configDir string) (string, error) {
	filename := filepath.Join(configDir, pkg.Name+PREBUILD_EXT)
	isExecutable, err := filecheck.IsExecutable(filename)
	if err != nil {
		return "", err
	}
	if isExecutable {
		return filename + " " + pkg.Path(), nil
	}
	return "", errors.New(filename + " is not executable")
}

// logPkgCmd runs the given command and logs its output to a file named
// `stage.log` in the package directory.
func logPkgCmd(
	pkg *sborepo.PkgNode,
	cmd, stage string,
	progress func(string, string),
) error {
	logFileName := filepath.Join(pkg.Path(), stage+".log")
	logFile := must.Create(logFileName)
	defer logFile.Close()
	progress(pkg.Name, strings.ToLower(stage)+" ["+logFileName+"]")
	err := cli.Run(cmd, "", logFile, logFile)
	if err != nil {
		skipDependents(pkg, "Failed "+stage+": "+logFileName, progress)
	}
	return err
}

func skipDependents(pkg *sborepo.PkgNode, msg string, progress func(string, string)) {
	progress(pkg.Name, msg)
	pkg.InGraph = false
	pkg.ProcessDependents(func(p *sborepo.PkgNode) {
		p.InGraph = false
		progress(p.Name, "Skipped; depends on "+pkg.Name)
	})
}

// logPkgCmdPanicOnFail runs the given command and logs its output to a file named
// `stage.log` in the package directory. It panics if the command fails.
func logPkgCmdPanicOnFail(
	pkg *sborepo.PkgNode,
	cmd, stage string,
	progress func(string, string),
) {
	if err := logPkgCmd(pkg, cmd, stage, progress); err != nil {
		panic(fmt.Errorf("%s failed: %w", cmd, err))
	}
}

func checkDownloads(dg sborepo.DependencyGraph, tmpDir string, progress func(string, string)) bool {
	ok := true
	dg.ProcessTopological(func(pkg *sborepo.PkgNode) {
		md5Sums := pkg.MD5Sums()
		for i, DownloadName := range pkg.DownloadNames() {
			fullPath := filepath.Join(tmpDir, DownloadName)
			checksum := ""
			if i < len(md5Sums) {
				checksum = md5Sums[i]
			}
			match, err := filecheck.HashMatches(fullPath, checksum, md5.New())
			var msg string
			switch {
			case err != nil:
				msg = err.Error()
			case !match:
				msg = fmt.Sprintf(
					"%s: MD5Sum Mismatch. Run with -%s to disable MD5Sum Check",
					DownloadName, MD5_FLAG,
				)
			default:
				continue
			}
			ok = false
			skipDependents(pkg, msg, progress)
		}
	})
	return ok
}

// slackwareVersion reads the Slackware version from the second field in the
// given /etc/slackware-version-like file and returns "current" if the version
// ends with a "+". Panics if the file cannot be read or is malformed.
func slackwareVersion(slackwareVersionFile string) string {
	content := strings.TrimSpace(string(must.ReadFile(slackwareVersionFile)))
	if strings.HasSuffix(content, "+") {
		return "current"
	}
	return strings.Fields(content)[1]
}

// buildPullCmd replaces the placeholders {MIRROR} and {VERSION} in pullCmd:
// On stable releases, they will be replaced by the second field of
// SLACKWARE_VERSION_FILE and the content of MIRROR, respectively. On the
// current branch, {VERSION} will be replaced by "current" and {MIRROR} will be
// replaced by MIRROR_CURRENT.
func buildPullCmd(pullCmd, slackwareVersionFile string) string {
	replaceMirror := strings.Contains(pullCmd, "{MIRROR}")
	replaceVersion := strings.Contains(pullCmd, "{VERSION}")
	if !replaceMirror && !replaceVersion {
		return pullCmd
	}
	slackwareVersion := slackwareVersion(slackwareVersionFile)
	mirror := MIRROR
	if replaceMirror && slackwareVersion == "current" {
		mirror = MIRROR_CURRENT
	}
	if replaceVersion {
		pullCmd = strings.ReplaceAll(pullCmd, "{VERSION}", slackwareVersion)
	}
	if replaceMirror {
		pullCmd = strings.ReplaceAll(pullCmd, "{MIRROR}", mirror)
	}
	return pullCmd
}
