package tool

import (
	"sort"
	"strings"

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

// Capability ids (Vikunja #452).
//
// A tool row is flat: one name, one scope, one enabled bit. Permission is
// therefore per name, and nothing groups. Hexis has spoken dotted capability
// ids since it existed, so the local surface was the odd one out — and the
// flat shape gets expensive around fifteen rows, when "what can she do to the
// house" stops being a question anyone can answer by reading a list.
//
// A capability id is scope.domain.action: homelab.docker.restart,
// house.lock.unlock, mcp_vikunja.vikunja.delete_task.
//
// DERIVED, not stored, for the same reason the risk tier is (risk.go): a
// derivation is one place to argue with, a column is whatever the last person
// to enable the row happened to type. The name stays the primary key and
// nothing about lookup or execution changes — this is a way to READ the
// allowlist, not a second allowlist.
type Capability struct {
	Scope  string
	Domain string
	Action string
}

// String renders the dotted id. An empty segment becomes "unknown" rather than
// collapsing, so an id always has three parts and a prefix match cannot
// accidentally widen.
func (c Capability) String() string {
	return capSegment(c.Scope) + "." + capSegment(c.Domain) + "." + capSegment(c.Action)
}

func capSegment(s string) string {
	s = strings.ToLower(strings.TrimSpace(s))
	s = strings.ReplaceAll(s, ".", "_")
	s = strings.ReplaceAll(s, " ", "_")
	if s == "" {
		return "unknown"
	}
	return s
}

// CapabilityOf derives the id of a tool row.
//
// The domain is the thing acted on and the action is what is done to it, read
// off whichever dispatch shape the row uses:
//
//   - a house row: the Home Assistant entity domain and the service, so
//     light.kitchen + turn_off becomes house.light.turn_off. Its scope is
//     "house" whatever the row says, because the entity id is what decides
//     what it touches.
//   - an MCP row: the server handle and the remote tool name.
//   - a process row: the program (path stripped) and its first subcommand, or
//     the tool name when the argv carries no second word.
func CapabilityOf(t ipc.Tool) Capability {
	if entityID, service, ok := smarthome.ParseCmd(t.Cmd); ok {
		// One parse of an entity id, in the package that owns the format.
		domain := smarthome.DomainOf(entityID)
		if domain == "" {
			domain = entityID
		}
		return Capability{Scope: "house", Domain: domain, Action: service}
	}
	if server, remote, ok := mcp.ParseCmd(t.Cmd); ok {
		return Capability{Scope: "mcp_" + server, Domain: server, Action: remote}
	}
	scope := t.Scope
	if scope == "" {
		scope = "homelab"
	}
	if len(t.Cmd) == 0 {
		// A proposal has no argv yet. It still gets an id, because "what did
		// she ask for" is exactly the question the proposed list answers.
		return Capability{Scope: scope, Domain: "unknown", Action: t.Name}
	}
	program := t.Cmd[0]
	if i := strings.LastIndex(program, "/"); i >= 0 {
		program = program[i+1:]
	}
	action := t.Name
	if len(t.Cmd) > 1 && !strings.HasPrefix(t.Cmd[1], "-") {
		action = t.Cmd[1]
	}
	return Capability{Scope: scope, Domain: program, Action: action}
}

// MatchCapability reports whether an id matches a pattern. A pattern is a
// dotted id whose segments may be "*", and a pattern with fewer segments than
// the id matches every id under it: "house" and "house.*" both cover
// house.lock.unlock.
//
// Prefix widening is deliberate and one-directional. "house.lock" covers every
// action on the locks; nothing lets a narrower id claim a wider pattern.
func MatchCapability(pattern string, c Capability) bool {
	want := strings.Split(strings.ToLower(strings.TrimSpace(pattern)), ".")
	got := strings.Split(c.String(), ".")
	if len(want) > len(got) {
		return false
	}
	for i, w := range want {
		if w == "*" || w == "" {
			continue
		}
		if w != got[i] {
			return false
		}
	}
	return true
}

// GroupByDomain buckets rows by "scope.domain" and returns the buckets in a
// stable order, which is what makes the allowlist readable past the point
// where a flat list stops being.
func GroupByDomain(tools []ipc.Tool) []CapabilityGroup {
	byKey := map[string][]ipc.Tool{}
	for _, t := range tools {
		c := CapabilityOf(t)
		key := capSegment(c.Scope) + "." + capSegment(c.Domain)
		byKey[key] = append(byKey[key], t)
	}
	out := make([]CapabilityGroup, 0, len(byKey))
	for k, v := range byKey {
		sort.Slice(v, func(i, j int) bool { return v[i].Name < v[j].Name })
		out = append(out, CapabilityGroup{Prefix: k, Tools: v})
	}
	sort.Slice(out, func(i, j int) bool { return out[i].Prefix < out[j].Prefix })
	return out
}

// CapabilityGroup — one scope.domain and the rows under it.
type CapabilityGroup struct {
	Prefix string
	Tools  []ipc.Tool
}
