package packages

import (
	"context"
	"fmt"
	"os/exec"
	"runtime"
	"time"
)

const collectTimeout = 2 * time.Minute

// Collect enumerates installed packages. Linux only: Windows agents are
// second-class by design, and vulnerability scanning there needs a different
// source, a different collector and a different matcher, all out of scope.
//
// The format strings below are raw string literals on purpose. The "\t" and
// "\n" reach dpkg-query and rpm as two characters each, and those tools do the
// interpreting themselves — Go must not consume the escapes first.
func Collect() (OSRelease, []Package, error) {
	if runtime.GOOS != "linux" {
		return OSRelease{}, nil, fmt.Errorf("package collection is linux-only, got %s", runtime.GOOS)
	}

	osrel, err := DetectOS()
	if err != nil {
		return OSRelease{}, nil, fmt.Errorf("detect os: %w", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), collectTimeout)
	defer cancel()

	switch {
	case have("dpkg-query"):
		out, err := run(ctx, "dpkg-query", "-W", "-f",
			`${Package}\t${Version}\t${Architecture}\t${source:Package}\t${db:Status-Status}\n`)
		if err != nil {
			return osrel, nil, err
		}
		return osrel, ParseDpkg(out), nil

	case have("rpm"):
		out, err := run(ctx, "rpm", "-qa", "--qf",
			`%{NAME}\t%{EPOCH}\t%{VERSION}-%{RELEASE}\t%{ARCH}\t%{SOURCERPM}\n`)
		if err != nil {
			return osrel, nil, err
		}
		return osrel, ParseRPM(out), nil

	case have("apk"):
		out, err := run(ctx, "apk", "info", "-v")
		if err != nil {
			return osrel, nil, err
		}
		return osrel, ParseAPK(out), nil

	default:
		return osrel, nil, fmt.Errorf("no supported package manager found")
	}
}

func have(bin string) bool {
	_, err := exec.LookPath(bin)
	return err == nil
}

func run(ctx context.Context, name string, args ...string) (string, error) {
	out, err := exec.CommandContext(ctx, name, args...).Output()
	if err != nil {
		return "", fmt.Errorf("%s: %w", name, err)
	}
	return string(out), nil
}
