package policy

import (
	"regexp"
	"strings"
)

// Risk class names, in escalation order (index = severity). A command's final
// risk class is the MAX of what the rules compute and what the caller
// declared — classification can only escalate, never de-escalate, mirroring
// the signal classifier's "policy can only lower autonomy, never raise it."
const (
	RiskReadOnly       = "read_only"
	RiskReversibleLow  = "reversible_low"
	RiskConfigMutation = "config_mutation"
	RiskDestructive    = "destructive"
)

var riskOrder = map[string]int{
	RiskReadOnly:       0,
	RiskReversibleLow:  1,
	RiskConfigMutation: 2,
	RiskDestructive:    3,
}

func riskRank(r string) int {
	if n, ok := riskOrder[r]; ok {
		return n
	}
	return riskOrder[RiskConfigMutation] // unknown declared risk: assume the safer-to-gate default
}

// destructivePatterns match commands that must always be treated as
// destructive, regardless of what the caller declares. Irreversible,
// data-loss, or fleet-wide-impact operations. Matched against the raw
// command text, case-insensitive.
var destructivePatterns = []*regexp.Regexp{
	regexp.MustCompile(`(?i)\brm\s+.*-[a-zA-Z]*r[a-zA-Z]*f|\brm\s+.*-[a-zA-Z]*f[a-zA-Z]*r`), // rm -rf / rm -fr (any flag order)
	regexp.MustCompile(`(?i)\bdd\s+.*of=`),
	regexp.MustCompile(`(?i)\bmkfs(\.\w+)?\b`),
	regexp.MustCompile(`(?i)\bwipefs\b`),
	regexp.MustCompile(`(?i)\bshred\b`),
	regexp.MustCompile(`(?i)\bpct\s+destroy\b`),
	regexp.MustCompile(`(?i)\bqm\s+destroy\b`),
	regexp.MustCompile(`(?i)\bzpool\s+destroy\b`),
	regexp.MustCompile(`(?i)\blvremove\b|\bvgremove\b|\bpvremove\b`),
	regexp.MustCompile(`(?i)\bdrop\s+(table|database|schema)\b`),
	regexp.MustCompile(`(?i)\btruncate\s+table\b`),
	regexp.MustCompile(`(?i)>\s*/dev/(sd|nvme|vd|hd)`),
	regexp.MustCompile(`(?i)\bshutdown\b|\breboot\b|\bhalt\b|\bpoweroff\b`),
	regexp.MustCompile(`(?i)\bformat\b.*\b(disk|partition|volume)\b`),
	regexp.MustCompile(`:\(\)\s*\{.*:\|:.*\}\s*;\s*:`), // fork bomb
	regexp.MustCompile(`(?i)\bchmod\s+-R\s+000\b|\bchmod\s+000\s+/`),
	regexp.MustCompile(`(?i)\biptables\s+-F\b|\bufw\s+disable\b`), // wipes firewall
	// secret/credential exfiltration — reading private keys, shadow, or age
	// keys is always destructive. (Piping a remote script into a shell via
	// curl|sh was previously here too, but that pattern is common for
	// legitimate installs — get.docker.com, convenience scripts — and
	// demoting it to config_mutation means loose assent can grant it without
	// a typed confirmation. The assent window covers the deploy case.)
	regexp.MustCompile(`(?i)\bcat\s+.*(id_rsa|id_ed25519|\.pem|shadow|\.age)\b`),
}

// readOnlyLeadPattern matches the leading command word (after env-var
// prefixes and a leading sudo) against a small allowlist of verbs that are
// safe to auto-run unattended: they inspect state and cannot mutate it.
// Compound commands (&&, ;, |, $(), backticks) are excluded from this fast
// path below — only a single simple command can qualify.
var readOnlyLeadPattern = regexp.MustCompile(
	`^(cat|less|head|tail|ls|stat|file|du|df|free|uptime|uname|hostname|whoami|id|ip|ss|netstat|ping|` +
		`journalctl|dmesg|ps|top|htop|env|printenv|echo|which|whereis|` +
		`grep|egrep|fgrep|rg|wc|sort|uniq|cut|tr|tee|` +
		`dpkg\s+(-l|-s|--list|--status)\b|apt\s+(list|search|show)\b|` +
		`systemctl\s+(status|is-active|is-enabled|is-failed|list-units)|` +
		`docker\s+(ps|images|inspect|logs|version|info|stats)|` +
		`pct\s+(status|config|list)|qm\s+(status|config|list)|pvesh\s+get|` +
		`git\s+(status|log|diff|show|branch|remote)|` +
		`curl\s+-.*-I\b|curl\s+.*--head\b)\b`)

// compoundSplitRe splits a command on shell chaining operators (;, &&, ||, |)
// so each segment can be individually classified. A piped or chained command
// where EVERY segment is a recognized read-only inspection verb is safe to
// auto-run — e.g. "systemctl status caddy; journalctl -u caddy -n 5" or
// "docker ps | grep caddy".
var compoundSplitRe = regexp.MustCompile(`\s*(?:&&|\|\||;|\|)\s*`)

// subshellRe matches command substitution ($() or backticks) that can hide
// arbitrary execution. A command using these never qualifies for the read-only
// fast path — the substituted content could do anything.
var subshellRe = regexp.MustCompile("\\$\\(|`")

// compoundOpPattern is retained for compatibility — matches any compound
// operator. (Previously used to block ALL compound commands from the read-only
// path; now the per-segment check is more precise.)
var compoundOpPattern = regexp.MustCompile("[;&|`]|\\$\\(")

// ClassifyCommand scores an arbitrary shell command for the general `run`
// primitive. It combines a rule-based verdict (destructive denylist first,
// then a read-only allowlist for simple inspection commands) with the
// caller's declared risk, and returns the more severe of the two — the
// classifier may only escalate, never de-escalate, so a model that
// under-declares risk (or an adversarial prompt) cannot talk its way past a
// genuinely dangerous command. Anything not matched by either rule defaults
// to config_mutation (escalate), per "when in doubt, escalate."
func ClassifyCommand(command, declaredRisk string) string {
	computed := computeCommandRisk(command)
	if declaredRisk == "" {
		return computed // no declaration to escalate with; computed's own escalate-by-default already applies
	}
	declared := normalizeRisk(declaredRisk)
	if riskRank(declared) > riskRank(computed) {
		return declared
	}
	return computed
}

func normalizeRisk(r string) string {
	if _, ok := riskOrder[r]; ok {
		return r
	}
	return RiskConfigMutation
}

func computeCommandRisk(command string) string {
	cmd := strings.TrimSpace(command)
	if cmd == "" {
		return RiskConfigMutation
	}

	for _, p := range destructivePatterns {
		if p.MatchString(cmd) {
			return RiskDestructive
		}
	}

	// Subshell substitution ($(), backticks) can hide arbitrary execution —
	// never auto-run, even if the visible verbs look read-only.
	if !subshellRe.MatchString(cmd) {
		if allSegmentsReadOnly(cmd) {
			return RiskReadOnly
		}
	}

	// Not obviously destructive, not a recognized read-only inspection —
	// default to the gated tier rather than guessing it's safe.
	return RiskConfigMutation
}

// allSegmentsReadOnly splits a compound command on chaining operators
// (;, &&, ||, |) and checks whether EVERY segment is a recognized read-only
// inspection verb. If so, the whole command is safe to auto-run. Any segment
// that isn't a recognized read-only verb disqualifies the whole command —
// the classifier errs toward gating, not guessing.
func allSegmentsReadOnly(cmd string) bool {
	segments := compoundSplitRe.Split(cmd, -1)
	for _, seg := range segments {
		seg = strings.TrimSpace(seg)
		if seg == "" {
			continue
		}
		// Strip a leading sudo/env assignment so "sudo cat /x" still matches.
		probe := seg
		probe = regexp.MustCompile(`^sudo\s+`).ReplaceAllString(probe, "")
		probe = regexp.MustCompile(`^(\w+=\S+\s+)+`).ReplaceAllString(probe, "")
		probe = strings.TrimSpace(probe)
		if !readOnlyLeadPattern.MatchString(probe) {
			return false
		}
	}
	return len(segments) > 0
}
