// This file is part of the adequate Debian-native package, and is available
// under the Expat license. For the full terms please see debian/copyright.

package main

import (
	"cmp"
	"fmt"
	"log"
	"os"
	"slices"
	"strings"
)

const (
	BROKEN_BINFMT_DETECTOR_TAG    = "broken-binfmt-detector"
	BROKEN_BINFMT_INTERPRETER_TAG = "broken-binfmt-interpreter"
)

type binfmtErr struct {
	tag         string
	pkg         string
	name        string
	interpreter string
	detector    string
	err         error
}

func (c binfmtErr) Error() string {
	if c == (binfmtErr{}) {
		return ""
	}
	var p string
	switch c.tag {
	case BROKEN_BINFMT_INTERPRETER_TAG:
		p = c.interpreter
	case BROKEN_BINFMT_DETECTOR_TAG:
		p = c.detector
	default:
		log.Fatalf("Unexpected binfmtErr tag value: %q", c.tag)
	}

	return fmt.Sprintf("%s: %s %s => %s (%s)", c.pkg, c.tag, c.name, p, c.err)
}

type notExecutableErr struct{}

func (ne notExecutableErr) Error() string {
	return "not executable"
}

type binfmtChecker struct {
	emitInterpreterTags bool
	emitDetectorTags    bool
	displayBinfmts      func(args []string) ([]string, error)
}

func newBinfmtChecker(tags tagFilter) binfmtChecker {
	return binfmtChecker{
		emitInterpreterTags: tags.shouldEmit(BROKEN_BINFMT_INTERPRETER_TAG),
		emitDetectorTags:    tags.shouldEmit(BROKEN_BINFMT_DETECTOR_TAG),
		displayBinfmts:      runCommand,
	}
}

func (cc binfmtChecker) check(pkg2files map[string][]string) []binfmtErr {
	if !cc.emitInterpreterTags && !cc.emitDetectorTags {
		return nil
	}

	pkgs := make(map[string]bool)
	for pkg := range pkg2files {
		p := strings.Split(pkg, ":")[0]
		pkgs[p] = true
	}

	formats := cc.binfmtFormats()
	var tags []binfmtErr
	for _, fmt := range formats {
		if cc.emitInterpreterTags {
			if fmt.name == "" || fmt.interpreter == "" || fmt.pkg == "" || !pkgs[fmt.pkg] {
				continue
			}

			err := os.ErrNotExist
			if pathExists(fmt.interpreter) {
				var isExec bool
				if isExec, err = isExecutable(fmt.interpreter); err == nil && isExec {
					continue
				}
				if err == nil {
					err = notExecutableErr{}
				}
			}
			tags = append(tags, binfmtErr{
				tag:         BROKEN_BINFMT_INTERPRETER_TAG,
				pkg:         fmt.pkg,
				name:        fmt.name,
				interpreter: fmt.interpreter,
				err:         err,
			})
		}

		if cc.emitDetectorTags {
			if fmt.detector == "" {
				continue
			}
			err := os.ErrNotExist
			if pathExists(fmt.detector) {
				var isExec bool
				if isExec, err = isExecutable(fmt.detector); err == nil && isExec {
					continue
				}
				if err == nil {
					err = notExecutableErr{}
				}
			}
			tags = append(tags, binfmtErr{
				tag:      BROKEN_BINFMT_DETECTOR_TAG,
				pkg:      fmt.pkg,
				name:     fmt.name,
				detector: fmt.detector,
				err:      err,
			})
		}
	}

	return tags
}

type format struct {
	name        string
	interpreter string
	detector    string
	pkg         string
}

func (cc binfmtChecker) binfmtFormats() []format {
	const updateBinfmts = "/usr/sbin/update-binfmts"
	if isExec, err := isExecutable(updateBinfmts); err != nil || !isExec {
		return nil
	}

	dispCmd := []string{updateBinfmts, "--display"}
	lines, err := cc.displayBinfmts(dispCmd)
	if err != nil {
		log.Fatalf(err.Error())
	}

	formats := make(map[string]format)
	var name string
	errMsgTempl := fmt.Sprintf("Unexpected output from %s: %%q", strings.Join(dispCmd, " "))

	// sample input:
	// python3.11 (enabled):
	//             package = python3.11
	//                type = magic
	//              offset = 0
	//               magic = \xa7\x0d\x0d\x0a
	//                mask =
	//         interpreter = /usr/bin/python3.11
	//            detector =

	for _, line := range lines {
		fields := strings.Fields(line)
		switch n := len(fields); {
		case n == 0:
			continue
		case n == 2 && strings.HasSuffix(fields[1], ":"):
			name = fields[0]
			if formats[name].name != "" {
				log.Fatalf(errMsgTempl, line)
			}
			formats[name] = format{name: name}
			continue
		case n == 3 && fields[1] == "=":
			key := fields[0]
			value := fields[2]
			if value == "" {
				continue
			}
			f := formats[name]
			if name == "" || key == "" || f.name == "" ||
				(key == "package" && f.pkg != "") ||
				(key == "interpreter" && f.interpreter != "") ||
				(key == "detector" && f.detector != "") {

				log.Fatalf(errMsgTempl, line)
			}

			switch key {
			case "interpreter":
				f.interpreter = value
			case "detector":
				f.detector = value
			case "package":
				f.pkg = value
			default:
				continue

			}
			formats[name] = f
			continue
		}
	}

	var fmts []format
	for _, f := range formats {
		fmts = append(fmts, f)
	}
	slices.SortFunc(fmts, func(a, b format) int {
		return cmp.Compare(a.name, b.name)
	})

	return fmts
}
