package capability

import (
	"context"
	"os"
)

// Status is one capability's live result — the wire shape the agent attaches to its hub report
// (HostReport.Capabilities). The hub mirrors this struct field-for-field and keys its alert on
// Critical+degraded. Reason is empty when ok.
//
// "inactive" (v0.86.0, DR-tier-by-default): a config-GATED capability whose plumbing is HEALTHY
// (binary present, sudo granted) but whose gating feature is disabled by configuration. Distinct
// from degraded on purpose — disabled ≠ broken; the hub renders it as a neutral chip, never red.
// Broken plumbing (binary missing / grant denied) stays DEGRADED even when the gate is off: an
// un-migrated box must never look deliberately disabled.
type Status struct {
	Name     string `json:"name"`
	Feature  string `json:"feature"`
	Critical bool   `json:"critical"`
	Status   string `json:"status"` // "ok" | "degraded" | "inactive"
	Reason   string `json:"reason,omitempty"`
}

const (
	StatusOK       = "ok"
	StatusDegraded = "degraded"
	StatusInactive = "inactive"
)

// ReasonInactive is the fixed reason string for the inactive state (the hub + operator docs
// reference it verbatim).
const ReasonInactive = "disabled by configuration"

// Runner is the minimal exec seam the probe needs (satisfied by proxmox.ExecRunner). The probe
// runs `sudo -n -l -- <binary> <args…>` LITERALLY — a sudo POLICY LIST that never executes the
// command — so the Runner MUST be a DIRECT runner (RunnerDirect), not the sudo-prepending one
// (else it would double-sudo). exit 0 ⇔ the command is permitted under the NOPASSWD allowlist.
type Runner interface {
	Run(ctx context.Context, name string, args ...string) (stdout, stderr []byte, err error)
}

// Prober checks the manifest against the live host. Exists defaults to an os.Stat check on the
// absolute binary path (what `command -v` would resolve for an absolute path) when nil.
// GateActive answers "is the feature behind this gate id configured on?" for GATED capabilities
// (Capability.GatedBy). nil, or a gate it answers true for, keeps the historical behavior; false
// downgrades a HEALTHY probe to StatusInactive (broken plumbing stays degraded regardless).
type Prober struct {
	Runner     Runner
	Exists     func(path string) bool  // nil → os.Stat
	GateActive func(gate string) bool // nil → every gate treated active
}

// Probe lists every manifest capability against the sudo policy and checks its binary exists,
// mapping to ok/degraded (§8 of the spec). It NEVER executes a probed command and NEVER returns a
// fatal error (serve-degraded): a probe failure is reported, not raised. If sudo itself is
// unavailable for the agent (the drop-in is missing / the user has no sudo at all), it collapses
// to ONE aggregate degraded signal instead of N identical ones.
func (p Prober) Probe(ctx context.Context) []Status {
	exists := p.Exists
	if exists == nil {
		exists = func(path string) bool { _, err := os.Stat(path); return err == nil }
	}
	caps := Manifest()

	// Preflight: a bare `sudo -n -l` lists the user's allowed commands. For our NOPASSWD service
	// user it exits 0; if it fails, the drop-in isn't installed (or sudo is gone) and EVERY vector
	// would individually fail — collapse to one aggregate signal so the operator gets one alert.
	if p.Runner != nil {
		if _, _, err := p.Runner.Run(ctx, "sudo", "-n", "-l"); err != nil {
			return []Status{{
				Name:     "sudo",
				Feature:  "the entire privileged surface (mount/format/pct/dnsmasq/lxc-info)",
				Critical: true,
				Status:   StatusDegraded,
				Reason:   "sudoers drop-in not installed / sudo unavailable",
			}}
		}
	}

	out := make([]Status, 0, len(caps))
	for _, c := range caps {
		s := Status{Name: c.Name, Feature: c.Feature, Critical: c.Critical, Status: StatusOK}
		switch {
		case !exists(c.Binary):
			s.Status, s.Reason = StatusDegraded, "binary not found"
		case p.Runner != nil && !p.granted(ctx, c):
			s.Status, s.Reason = StatusDegraded, "sudo policy denied"
		}
		// Config gate (v0.86.0): only a HEALTHY probe is downgraded to inactive — a degraded one
		// stays degraded (missing binary/grant = un-migrated or mis-installed box, never "off").
		if s.Status == StatusOK && c.GatedBy != "" && p.GateActive != nil && !p.GateActive(c.GatedBy) {
			s.Status, s.Reason = StatusInactive, ReasonInactive
		}
		out = append(out, s)
	}
	return out
}

// granted reports whether `sudo -n -l -- <binary> <reprArgs…>` is permitted (exit 0). List-mode is
// side-effect-free — the command is matched against the policy, never run.
func (p Prober) granted(ctx context.Context, c Capability) bool {
	args := append([]string{"-n", "-l", "--", c.Binary}, c.ReprArgs...)
	_, _, err := p.Runner.Run(ctx, "sudo", args...)
	return err == nil
}

// Summarize returns (okCount, total, degraded) for logging. degraded lists DEGRADED statuses
// only — inactive is a deliberate, healthy state and must not land in the error log (it is
// counted via len(statuses)-ok-len(degraded) by callers that want it).
func Summarize(statuses []Status) (ok, total int, degraded []Status) {
	total = len(statuses)
	for _, s := range statuses {
		switch s.Status {
		case StatusOK:
			ok++
		case StatusDegraded:
			degraded = append(degraded, s)
		}
	}
	return ok, total, degraded
}
