//go:build !windows

package client

import (
	"fmt"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"strconv"
	"strings"
	"time"
)

func collectPosture() *PostureReport {
	r := &PostureReport{AgentServiceOK: boolPtr(true)}

	// ── Firewall ───────────────────────────────────────────────────────────────
	fwActive := probeUnixFirewall()
	r.FirewallDomain = &fwActive
	r.FirewallPrivate = &fwActive
	// Public profile has no direct Linux equivalent; leave nil.

	// ── AV / security daemons ─────────────────────────────────────────────────
	avProducts := probeUnixAV()
	r.AVProducts = avProducts
	avOK := fwActive || len(avProducts) > 0
	r.DefenderEnabled = &avOK

	// ── SSH ───────────────────────────────────────────────────────────────────
	ssh := probeSSH()
	r.SSHListening = &ssh

	// ── Patch age ─────────────────────────────────────────────────────────────
	days, dateStr := probeUnixPatchAge()
	if days >= 0 {
		r.LastPatchDays = &days
		recent := days <= 30
		r.PatchRecent = &recent
		if dateStr != "" {
			r.LastPatch = &dateStr
		}
	}

	// ── Pending updates ───────────────────────────────────────────────────────
	pending := probeUnixPendingUpdates()
	r.PendingUpdates = &pending

	// ── Reboot pending ────────────────────────────────────────────────────────
	rp := probeUnixRebootPending()
	r.RebootPending = &rp

	// ── T1007 Service Discovery ───────────────────────────────────────────────
	r.Services = probeUnixServices()

	// ── Elevation ─────────────────────────────────────────────────────────────
	elevated := probeUnixElevated()
	r.AgentElevated = &elevated

	r.PostureScore = computePostureScore(r)
	return r
}

// selfServiceName returns the base name of the running binary (no extension).
func selfServiceName() string {
	exe, err := os.Executable()
	if err != nil || exe == "" {
		return "aetherforge"
	}
	return filepath.Base(exe)
}

// probeUnixServices checks a fixed allowlist via systemctl.
// Only the services relevant to this project are queried (T1007-compliant).
func probeUnixServices() []ServiceStatus {
	self := selfServiceName()
	allowlist := []string{"sshd", "ssh", "cloudflared", "wuauserv", "ufw", "fail2ban", self}

	// Deduplicate
	seen := map[string]bool{}
	var names []string
	for _, n := range allowlist {
		if n != "" && !seen[n] {
			seen[n] = true
			names = append(names, n)
		}
	}

	var out []ServiceStatus
	queried := map[string]bool{}
	for _, name := range names {
		if queried[name] {
			continue
		}
		queried[name] = true

		status := "not_found"
		startType := "unknown"

		// ActiveState
		if activeOut, err := exec.Command("systemctl", "is-active", name).CombinedOutput(); err == nil {
			active := strings.TrimSpace(string(activeOut))
			switch active {
			case "active":
				status = "running"
			case "inactive", "failed", "dead":
				status = "stopped"
			default:
				// service exists but is in an odd state
				if active != "unknown" {
					status = "stopped"
				}
			}
		}

		// UnitFileState (start type)
		if enableOut, err := exec.Command("systemctl", "is-enabled", name).CombinedOutput(); err == nil || status != "not_found" {
			switch strings.TrimSpace(string(enableOut)) {
			case "enabled":
				startType = "auto"
			case "disabled":
				startType = "disabled"
			case "static", "manual":
				startType = "manual"
			}
		}

		out = append(out, ServiceStatus{
			Name:      name,
			Status:    status,
			StartType: startType,
		})
	}
	return out
}

// probeUnixFirewall returns true if any host firewall appears active.
func probeUnixFirewall() bool {
	if out, _ := exec.Command("ufw", "status").CombinedOutput(); strings.Contains(strings.ToLower(string(out)), "status: active") {
		return true
	}
	if out, _ := exec.Command("systemctl", "is-active", "firewalld").CombinedOutput(); strings.TrimSpace(string(out)) == "active" {
		return true
	}
	if out, err := exec.Command("/bin/sh", "-c", "iptables -L -n 2>/dev/null | wc -l").CombinedOutput(); err == nil {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 8 {
			return true
		}
	}
	if out, _ := exec.Command("/bin/sh", "-c", "nft list ruleset 2>/dev/null | wc -l").CombinedOutput(); true {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n > 3 {
			return true
		}
	}
	return false
}

// probeUnixAV returns names of active security daemons.
func probeUnixAV() []string {
	type entry struct{ svc, label string }
	candidates := []entry{
		{"clamav-daemon", "ClamAV"}, {"clamd", "ClamAV"},
		{"fail2ban", "Fail2Ban"}, {"rkhunter", "RKHunter"},
		{"chkrootkit", "Chkrootkit"}, {"auditd", "auditd"},
	}
	seen := map[string]bool{}
	var found []string
	for _, c := range candidates {
		out, _ := exec.Command("systemctl", "is-active", c.svc).CombinedOutput()
		if strings.TrimSpace(string(out)) == "active" && !seen[c.label] {
			seen[c.label] = true
			found = append(found, c.label)
		}
	}
	return found
}

// probeUnixPatchAge returns (days since last update, ISO date string).
// Returns (-1, "") when unknown.
func probeUnixPatchAge() (int, string) {
	stamps := []string{
		"/var/lib/apt/periodic/update-success-stamp",
		"/var/cache/apk/lastupdate",
	}
	for _, path := range stamps {
		if info, err := os.Stat(path); err == nil {
			d := int(time.Since(info.ModTime()).Hours() / 24)
			if d < 0 {
				d = 0
			}
			return d, info.ModTime().Format("2006-01-02")
		}
	}
	// YUM/DNF history
	if out, err := exec.Command("/bin/sh", "-c",
		`yum history info 2>/dev/null | grep "Begin time" | head -1 | awk '{print $3, $4}'`).CombinedOutput(); err == nil {
		line := strings.TrimSpace(string(out))
		if line != "" {
			if t, err := time.Parse("2006-01-02 15:04", line); err == nil {
				return int(time.Since(t).Hours() / 24), t.Format("2006-01-02")
			}
		}
	}
	// rpm last installed
	if out, err := exec.Command("/bin/sh", "-c", "rpm -qa --last 2>/dev/null | head -1").CombinedOutput(); err == nil {
		parts := strings.Fields(string(out))
		if len(parts) >= 5 {
			dateStr := strings.Join(parts[len(parts)-5:], " ")
			if t, err := time.Parse("Mon Jan 2 15:04:05 2006", dateStr); err == nil {
				return int(time.Since(t).Hours() / 24), t.Format("2006-01-02")
			}
		}
	}
	return -1, ""
}

// probeUnixPendingUpdates returns the count of available package updates.
// Returns -1 if the count cannot be determined.
func probeUnixPendingUpdates() int {
	// APT
	if out, err := exec.Command("/bin/sh", "-c",
		"apt list --upgradable 2>/dev/null | grep -c upgradable").CombinedOutput(); err == nil {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
			return n
		}
	}
	// APT check (outputs "pkg;security" lines)
	if out, err := exec.Command("/bin/sh", "-c",
		"/usr/lib/update-notifier/apt-check 2>&1 | cut -d';' -f1").CombinedOutput(); err == nil {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil && n >= 0 {
			return n
		}
	}
	// YUM/DNF
	if out, err := exec.Command("/bin/sh", "-c",
		"yum check-update --quiet 2>/dev/null | grep -c '^[a-zA-Z]'").CombinedOutput(); err == nil {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
			return n
		}
	}
	// apk (Alpine)
	if out, err := exec.Command("/bin/sh", "-c",
		"apk version -l '<' 2>/dev/null | wc -l").CombinedOutput(); err == nil {
		if n, err := strconv.Atoi(strings.TrimSpace(string(out))); err == nil {
			return n
		}
	}
	return -1
}

// probeUnixRebootPending checks kernel and package signals for a required reboot.
func probeUnixRebootPending() bool {
	// Debian/Ubuntu explicit flag
	if _, err := os.Stat("/var/run/reboot-required"); err == nil {
		return true
	}
	// Kernel update check: compare running vs installed
	if out, err := exec.Command("uname", "-r").CombinedOutput(); err == nil {
		running := strings.TrimSpace(string(out))
		// Check if a newer kernel package exists
		if out2, err := exec.Command("/bin/sh", "-c",
			fmt.Sprintf("ls /boot/vmlinuz-* 2>/dev/null | grep -v '%s' | wc -l", running)).CombinedOutput(); err == nil {
			if n, err := strconv.Atoi(strings.TrimSpace(string(out2))); err == nil && n > 0 {
				return true
			}
		}
	}
	return false
}

// probeUnixElevated returns true when running as root (uid 0).
func probeUnixElevated() bool {
	u, err := user.Current()
	if err != nil {
		return false
	}
	return u.Uid == "0"
}
