package main

import (
	"fmt"
	"strings"
)

const NAME_COLLISION_TAG = "program-name-collision"

type nameCollisionErr struct {
	pkg              string
	programName      string
	otherProgramName string
	isBashBuiltin    bool
}

func (n nameCollisionErr) Error() string {
	if n.isBashBuiltin {
		return fmt.Sprintf("%s: %s %s (bash builtin command)", n.pkg, NAME_COLLISION_TAG, n.programName)
	}

	return fmt.Sprintf("%s: %s %s %s", n.pkg, NAME_COLLISION_TAG, n.programName, n.otherProgramName)
}

type collisionChecker struct {
	sysDirs        []string
	acceptList     map[string]string
	bashBuiltins   map[string]bool
	bashAcceptList map[string]bool
	emit           bool
}

func newCollisionChecker(tags tagFilter) collisionChecker {
	cc := collisionChecker{
		sysDirs: []string{"/usr/sbin", "/usr/bin", "/sbin", "/bin", "/usr/games"},
		acceptList: map[string]string{
			// molly-guard: bugs #733213, #660064
			"/usr/sbin/halt":     "molly-guard",
			"/usr/sbin/poweroff": "molly-guard",
			"/usr/sbin/reboot":   "molly-guard",
			"/usr/sbin/shutdown": "molly-guard",
			// safe-rm
			"/usr/bin/rm": "safe-rm",
		},
		// Do not edit the following line manually: run
		// private/update-bash-builtin-commands instead.
		bashBuiltins:   strToBoolMap(". [ alias bg bind break builtin caller case cd command compgen complete compopt continue coproc declare dirs disown echo enable eval exec exit export false fc fg for function getopts hash help history if jobs kill let local logout mapfile popd printf pushd pwd read readarray readonly return select set shift shopt source suspend test time times trap true type typeset ulimit umask unalias unset until variables wait while"),
		bashAcceptList: strToBoolMap("coreutils time procps"),
		emit:           tags.shouldEmit(NAME_COLLISION_TAG),
	}

	return cc
}

func (cc collisionChecker) check(pkg2files map[string][]string) []nameCollisionErr {
	var tags []nameCollisionErr

	for pkg, files := range pkg2files {
		pkgFiles := strSliceToBoolMap(files)
		for _, file := range files {
			for _, sdir := range cc.sysDirs {
				suffix, ok := strings.CutPrefix(file, sdir+"/")
				if !ok {
					continue
				}
				if !pathExists(file) {
					continue
				}
				if !cc.bashAcceptList[pkg] && cc.bashBuiltins[suffix] {
					tags = append(tags, nameCollisionErr{
						pkg:           pkg,
						programName:   file,
						isBashBuiltin: true,
					})
				}

				for _, dDir := range cc.sysDirs {
					dFile := dDir + "/" + suffix
					_, acceptListed := cc.acceptList[dFile]

					if !pathExists(dFile) ||
						pkgFiles[dFile] ||
						acceptListed ||
						cc.acceptList[file] == pkg ||
						sameFile(file, dFile) {

						continue
					}
					tags = append(tags, nameCollisionErr{
						pkg:              pkg,
						programName:      file,
						otherProgramName: dFile,
					})
				}
				break
			}
		}
	}

	return tags
}

func strToBoolMap(s string) map[string]bool {
	return strSliceToBoolMap(strings.Split(s, " "))
}

func strSliceToBoolMap(keys []string) map[string]bool {
	m := make(map[string]bool, len(keys))
	for _, k := range keys {
		m[k] = true
	}

	return m
}
