// 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 (
	"bytes"
	"crypto/md5"
	"encoding/hex"
	"errors"
	"fmt"
	"log"
	"os"
	"regexp"
	"strings"
)

const OBSOLETE_CONFFILE_TAG = "obsolete-conffile"

type conffileErr struct {
	pkg  string
	path string
}

func (c conffileErr) Error() string {
	if c == (conffileErr{}) {
		return ""
	}
	return fmt.Sprintf("%s: %s %s", c.pkg, OBSOLETE_CONFFILE_TAG, c.path)
}

type dpkgQueryFunc func([]string) ([]string, error)
type readFileFunc func(string) ([]byte, error)
type packageConffileChecker struct {
	dqFunc   dpkgQueryFunc
	readFile readFileFunc
	emit     bool
}

func newPackageConffileChecker(tags tagFilter) packageConffileChecker {
	return packageConffileChecker{
		dqFunc:   runCommand,
		readFile: os.ReadFile,
		emit:     tags.shouldEmit(OBSOLETE_CONFFILE_TAG),
	}
}

func (cc packageConffileChecker) check(pkgs []string) []error {
	if !cc.emit {
		return nil
	}
	// Try both ${binary:Package} and ${Package}; the former gives us
	// architecture information, but the later works with pre-multiarch dpkg.
	lines, err := cc.dqFunc(append([]string{
		"/usr/bin/dpkg-query",
		"-Wf", "${binary:Package},${Package}\n${Conffiles}\n",
		"--"},
		pkgs...))
	if err != nil {
		log.Fatal(err)
	}

	// sample input:
	// zsh-common,zsh-common
	//  /etc/zsh/newuser.zshrc.recommended dac3563a2ddd13e8027b1861d415f3d4 obsolete
	//  /etc/zsh/zlogin 48032df2ace0977f2491b016e3c421a3
	pkgLine := regexp.MustCompile("^,?([^,\t\n\f\r ]+)")

	var pkg string
	obsFiles := make(map[string]bool)
	pkg2files := make(map[string][]string)
	for _, line := range lines {
		if m := pkgLine.FindStringSubmatch(line); len(m) == 2 {
			pkg = m[1]
			continue
		}

		// sample input:
		// 	/etc/default/duplicate 964d39a52b30de6627ba346001730f03 obsolete
		var conffile struct {
			path     string
			checksum string
			obsolete bool
		}
		fields := strings.Fields(line)
		if n := len(fields); n < 2 {
			continue
		} else if n == 3 && fields[2] == "obsolete" {
			conffile.obsolete = true
		}
		conffile.path = fields[0]
		conffile.checksum = fields[1]

		switch {
		case pkg == "":
			log.Fatalf("Unexpected output from dpkg-query -W")
		case !conffile.obsolete:
			// Work-around for dpkg bug #645849: don't consider a conffile
			// obsolete if it's listed as non-obsolete in a different
			// package.
			obsFiles[conffile.path] = false
		case conffile.obsolete:
			if _, ok := obsFiles[conffile.path]; ok {
				// File has already been marked by another package.
				continue
			}
			obsFiles[conffile.path] = true
			// Do not emit tag for obsolete but locally modified
			// conffiles.
			if !cc.matchesChecksum(conffile.path, conffile.checksum) {
				continue
			}
			pkg2files[pkg] = append(pkg2files[pkg], conffile.path)
		}
	}

	var errs []error
	for _, pkg := range pkgs {
		for _, f := range pkg2files[pkg] {
			if !obsFiles[f] {
				continue
			}
			errs = append(errs, conffileErr{
				pkg:  pkg,
				path: f,
			})

		}
	}

	return errs
}

// matchesChecksum returns true if path's contents match checksum or if the file
// does not exist.
//
// checksum is a hex string.
//
//
// Calls log.Fatal() if the provided checksum is invalid (not hex, or not
// md5.Size bytes long). A warning is printed to stderr if the file cannot be
// read for whatever reason (e.g. permissions, path not being a file).
func (cc packageConffileChecker) matchesChecksum(path, checksum string) bool {
	sum, err := hex.DecodeString(checksum)
	if err != nil {
		log.Fatalf("Given md5sum %q is not a valid hex number: %v",
			checksum, err)
	}

	if n := len(sum); n != md5.Size {
		log.Fatalf("Given md5 checksum %q is %d long; expected %d",
			checksum, n, md5.Size)
	}

	buf, err := cc.readFile(path)
	if errors.Is(err, os.ErrNotExist) {
		return true
	}
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to read %s: %v\n", path, err)
	}

	actual := md5.Sum(buf)
	return bytes.Equal(actual[:] /* convert array to slice */, []byte(sum))
}
