package mcp

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"regexp"
	"strconv"
	"strings"
)

// CmdPrefix is the reserved first argv element that marks an allowlist row as
// an MCP call rather than a process. An MCP tool row looks like
//
//	name: "vikunja_list_tasks"   cmd: ["mcp", "vikunja", "list_tasks"]
//
// which is why there is no new column and no migration: the store, the /tools
// page, ProposeTool, EnableTool, DisableTool, the act matcher and the confirm
// turn all keep working unchanged. The executor is the only place that has to
// know the difference, and it is one branch on Cmd[0].
//
// The rest of the allowlist discipline is inherited whole: a row that is not
// status='enabled' does not run, and a row marked destructive does not run on
// first hearing. Nothing here can enable itself — discovery only proposes.
const CmdPrefix = "mcp"

// Cmd builds the argv encoding for a discovered tool.
func Cmd(server, tool string) []string { return []string{CmdPrefix, server, tool} }

// ParseCmd recognises an MCP allowlist row. ok=false for an ordinary process
// tool, which is what almost every row is.
func ParseCmd(cmd []string) (server, tool string, ok bool) {
	if len(cmd) != 3 || cmd[0] != CmdPrefix {
		return "", "", false
	}
	if cmd[1] == "" || cmd[2] == "" {
		return "", "", false
	}
	return cmd[1], cmd[2], true
}

var notName = regexp.MustCompile(`[^a-z0-9_]+`)

// LocalName is the allowlist name for a discovered tool: the server handle, an
// underscore, the remote name, lowercased and stripped of anything that is not
// a word character. Namespacing by server is what keeps two servers that both
// offer "search" from colliding, and what makes the provenance of a row on the
// /tools page obvious without opening the diff.
func LocalName(server, tool string) string {
	clean := func(s string) string {
		return strings.Trim(notName.ReplaceAllString(strings.ToLower(strings.TrimSpace(s)), "_"), "_")
	}
	s, t := clean(server), clean(tool)
	switch {
	case s == "":
		return t
	case t == "":
		return s
	}
	return s + "_" + t
}

// Scope is the store scope for a server's rows, so the /tools page can group
// them and a human can tell at a glance where a capability came from.
func Scope(server string) string { return "mcp:" + server }

// Fingerprint is the declared shape of a discovered tool: its name, its
// description, its input schema and its readOnlyHint, hashed.
//
// It exists because an allowlist row cannot pin an MCP tool's behaviour. The
// row's cmd is ["mcp", server, tool], a reference to a name the REMOTE server
// owns and may redefine — the row does not have to change for the tool to
// become something else. The fingerprint is what Kami actually approved, so a
// later discovery can tell "same tool" from "same name".
//
// The schema is canonicalised through a decode and re-encode, so a server that
// reorders its JSON keys or changes its whitespace does not read as a
// redefinition. Unparseable schema bytes are hashed as they arrived.
func Fingerprint(t Tool) string {
	schema := ""
	if len(t.InputSchema) > 0 {
		var any any
		if json.Unmarshal(t.InputSchema, &any) == nil {
			if raw, err := json.Marshal(any); err == nil {
				schema = string(raw)
			}
		}
		if schema == "" {
			schema = string(t.InputSchema)
		}
	}
	h := sha256.New()
	for _, part := range []string{t.Name, t.Description, schema, strconv.FormatBool(t.ReadOnly)} {
		h.Write([]byte(part))
		h.Write([]byte{0})
	}
	return hex.EncodeToString(h.Sum(nil))
}
