package pkg

import (
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strings"

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

func pkgNameFromFile(filename string) []sborepo.PkgName {
	f, err := os.Open(filename)
	if err != nil {
		return nil
	}
	defer f.Close()
	return cli.StripCommentsAndTokenize(f)
}

// 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.Package) { pkg.Write(dest) })
	return nil
}

// promptIfReadmeRequired prompts the user to display the README file
// if it is listed as required in the info file.
func promptIfReadmeRequired(pkg *sborepo.Package, dg sborepo.DependencyGraph, ui UI) {
	if !pkg.IsReadmeRequired() || !ui.Prompt(pkg.Name+
		": Requires to read the README file. Do you want "+
		"to read it (and optionally skip the package) now?",
	) {
		return
	}
	must.Must(ui.Display(pkg.AllReadmes()))
	var (
		depNames = sborepo.Names(dg.IterDependents(pkg.Name))
		wantSkip = fmt.Sprintf("Do you want to skip " + pkg.Name)
	)
	if len(depNames) > 1 {
		wantSkip += fmt.Sprintf(" and its dependents %v", depNames[1:])
	}
	if ui.Prompt(wantSkip) {
		dg.RemoveDependents(pkg.Name)
	}
}

func buildCmd(pkg *sborepo.Package, 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.Package, configDir string) (string, error) {
	var (
		filename          = filepath.Join(configDir, pkg.Name+PREBUILD_EXT)
		isExecutable, err = filecheck.IsExecutable(filename)
	)
	switch {
	case err != nil:
		return "", err
	case isExecutable:
		return filename + " " + pkg.Path(), nil
	default:
		return "", errors.New(filename + " is not executable")
	}
}

func skipDependents(
	pkg *sborepo.Package,
	dg sborepo.DependencyGraph,
	msg string,
	progress cli.ProgressReporter,
) {
	progress(pkg.Name, msg)
	for _, pn := range dg.RemoveDependents(pkg.Name) {
		if pn != pkg.Name {
			progress(pn, "Skipped; depends on "+pkg.Name)
		}
	}
}

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

// 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 {
	var (
		replaceMirror  = strings.Contains(pullCmd, "{MIRROR}")
		replaceVersion = strings.Contains(pullCmd, "{VERSION}")
	)
	if !replaceMirror && !replaceVersion {
		return pullCmd
	}
	var (
		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
}

// 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]
}
