package tool

import (
	"strings"

	"github.com/kami/maven/internal/ipc"
	"github.com/kami/maven/internal/mcp"
	"github.com/kami/maven/internal/smarthome"
)

// Risk tiers (Vikunja #449).
//
// What existed before this file was a mechanism and no policy: one
// `Destructive` boolean per row, set by whoever ticked the checkbox on /tools.
// Nothing said which acts are destructive, whether a confirmed act stays
// confirmed, or what a new tool domain inherits — so every domain answered
// those questions for itself, and two of them answered differently.
//
// The tiers below are the policy. They are derived from the row, not stored:
// a derivation can be argued with and corrected in one place, while a column
// is whatever the last person to enable the tool believed.
//
// The three questions, answered once:
//
//   - WHICH ACTS ARE DESTRUCTIVE. A house row always is, because there is no
//     read-only way to turn the heating off. A row whose argv names one of the
//     irreversible verbs always is, whatever the checkbox says. Everything else
//     is what the row was enabled as.
//   - DOES A CONFIRMED ACT STAY CONFIRMED. No. Never, at any tier. A
//     confirmation binds one capability, one target and one argument list, and
//     it expires with the parked turn (confirmTTL, 90s). "Same act again" is a
//     new act and costs a new turn. A sticky confirm is a standing grant, and
//     nothing on the voice path may hold one.
//   - WHAT A NEW DOMAIN INHERITS. The default is TierDestructive, not
//     TierSafe. A dispatch shape this file does not recognise gets the confirm
//     turn — a new domain must argue its way DOWN to running freely, never up
//     to needing a confirm.
type Risk string

const (
	// TierSafe — a read, or a mutation the owner can undo by saying the
	// opposite. Runs on first hearing.
	TierSafe Risk = "safe"
	// TierDestructive — it changes something real and undoing it takes work.
	// One confirm turn, every time, never remembered.
	TierDestructive Risk = "destructive"
	// TierIrreversible — the thing it acts on does not come back: a wipe, a
	// format, a delete with no bin behind it. A confirm turn is not enough,
	// because the whole chain that proposed it — an STT guess, a router guess,
	// a fuzzy allowlist match — has a spoken "да" as its only check. She names
	// the gap and he runs it himself.
	TierIrreversible Risk = "irreversible"
)

// Policy — what a tier requires of the act path.
//
// There is deliberately no "sticky for" field. Non-stickiness is the policy,
// and a knob that could turn it off would be the thing to argue with instead
// of the rule.
type Policy struct {
	// Confirm — the act does not run on first hearing.
	Confirm bool
	// VoiceMayRun — a spoken confirmation is enough authority to run it.
	VoiceMayRun bool
}

// PolicyFor returns the requirements of a tier. An unknown tier is treated as
// destructive, for the same reason the default derivation is.
func PolicyFor(r Risk) Policy {
	switch r {
	case TierSafe:
		return Policy{Confirm: false, VoiceMayRun: true}
	case TierIrreversible:
		return Policy{Confirm: true, VoiceMayRun: false}
	default:
		return Policy{Confirm: true, VoiceMayRun: true}
	}
}

// irreversibleVerbs — argv heads and subcommands that destroy the thing they
// name. Matched as whole argv elements, never as substrings: "rm" must not
// fire on "/usr/bin/rmdir-report" and "drop" must not fire on "dropbox".
//
// The list is short on purpose. It is not a sandbox and it does not try to be
// one — an enabled row can already run anything the daemon's user can run.
// What it is, is the set of words that mean "and then it is gone", so that the
// one act nobody can walk back is the one act a spoken "да" cannot authorise.
var irreversibleVerbs = map[string]bool{
	"rm": true, "rmdir": true, "shred": true, "srm": true,
	"mkfs": true, "fdisk": true, "parted": true, "wipefs": true,
	"dd": true, "format": true,
	"drop": true, "drop-database": true, "destroy": true, "purge": true,
	"prune": true, "truncate": true,
}

// RiskOfCapability — the tier of a Hexis capability, which Hexis decides.
//
// Everything below this comment in RiskOf is a derivation, and a derivation is
// only honest where nobody else holds the answer. Hexis does hold it: the
// capability carries risk, read_only and requires_confirmation, and its own
// contract says requires_confirmation is server-derived from the tier and never
// settable by a caller. Deriving a second opinion here is the same defect as
// inventing a local fact key for something Nexus resolves — two answers, one of
// them stale, and the wrong one authorising an act (Vikunja #523).
//
// So this reads rather than decides. The three arguments are Capability.Risk,
// Capability.ReadOnly and Capability.RequiresConfirmation, passed as plain
// values so internal/tool keeps no dependency on the Hexis client.
//
// The one judgement left is what to do with an answer we cannot read. It goes
// up, never down: an unrecognised tier gets the confirm turn, the same default
// a dispatch shape RiskOf does not know gets. And requires_confirmation may
// only raise — a capability that calls itself safe and then asks for a confirm
// is telling us two things, and the cautious one wins.
func RiskOfCapability(risk string, readOnly, requiresConfirmation bool) Risk {
	switch Risk(strings.ToLower(strings.TrimSpace(risk))) {
	case TierIrreversible:
		return TierIrreversible
	case TierDestructive:
		return TierDestructive
	case TierSafe:
		// Safe and mutating is a contradiction, and so is safe with a confirm
		// required. Either way the act changes something.
		if requiresConfirmation || !readOnly {
			return TierDestructive
		}
		return TierSafe
	case "":
		// No tier declared. Fall back to the shape Hexis did give us: a
		// read-only capability that wants no confirm is a read, and anything
		// else takes the confirm turn.
		if readOnly && !requiresConfirmation {
			return TierSafe
		}
		return TierDestructive
	default:
		// A word this file has never seen. It is not safe by default.
		return TierDestructive
	}
}

// RiskOf derives the tier of a locally enabled tool row — a shell command, an
// MCP call or a house service. Nothing here is a Hexis capability, and nothing
// upstream has an opinion about a row the owner ticked on /tools, which is why
// this one derives and RiskOfCapability reads.
func RiskOf(t ipc.Tool) Risk {
	if isIrreversible(t.Cmd) {
		return TierIrreversible
	}
	// A house row is a physical change to the flat, and the confirm turn on it
	// is structural rather than a column: /tools writes the checkbox straight
	// through on enable, so unticking it once turned an unlock into a row that
	// ran on first hearing. Nothing any surface writes removes the second turn
	// from a physical device.
	if _, _, ok := smarthome.ParseCmd(t.Cmd); ok {
		return TierDestructive
	}
	// An MCP row is a call to somebody else's server. It is enabled with a
	// fingerprint of what it declared at approval time (Vikunja #251), and the
	// tier tracks the same flag every other row uses — the point of this branch
	// is that it is NOT special-cased into running freely.
	if _, _, ok := mcp.ParseCmd(t.Cmd); ok {
		if t.Destructive {
			return TierDestructive
		}
		return TierSafe
	}
	if t.Destructive {
		return TierDestructive
	}
	if len(t.Cmd) == 0 {
		// Not a shape this file knows how to read. The default is the confirm
		// turn: a new domain argues its way down, not up.
		return TierDestructive
	}
	return TierSafe
}

// isIrreversible reports whether any argv element is one of the verbs that
// destroys what it names. Every element, not just the head: "sudo rm" and
// "docker volume prune" both hide the verb behind a wrapper.
func isIrreversible(cmd []string) bool {
	for _, arg := range cmd {
		word := strings.ToLower(strings.TrimSpace(arg))
		// Take the last path element, so /bin/rm reads as rm.
		if i := strings.LastIndex(word, "/"); i >= 0 {
			word = word[i+1:]
		}
		if irreversibleVerbs[word] {
			return true
		}
	}
	return false
}
