// 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"
	"regexp"
	"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
}

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

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 := 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 binfmtFormats() []format {
	updateBinfmts := "/usr/sbin/update-binfmts"
	if isExec, err := isExecutable(updateBinfmts); err != nil || !isExec {
		return nil
	}

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

	formats := make(map[string]format)
	var name string
	headerRE := regexp.MustCompile("^(" + nonSpaceToken + ") .*:$")
	keyValRE := regexp.MustCompile("^ +([a-z]+) = (.*)$")
	errMsgTempl := fmt.Sprintf("Unexpected output from %s: %%q", strings.Join(dispCmd, " "))

	for _, line := range lines {
		if m := headerRE.FindStringSubmatch(line); len(m) == 2 {
			name = m[1]
			if formats[name].name != "" {
				log.Fatalf(errMsgTempl, line)
			}
			formats[name] = format{name: name}
			continue
		}
		if m := keyValRE.FindStringSubmatch(line); len(m) == 3 {
			key, value := m[1], m[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
		}
		log.Fatalf(errMsgTempl, line)
	}

	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
}
