package config

import (
	"time"

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

// MCPConfig — the MCP client block. Servers are dark until one has
// `"enabled": true`, and a discovered tool is only ever PROPOSED: Kami enables
// it on /tools, on the authed surface, exactly as he would a shell tool. The
// voice path can never grant a capability to itself.
type MCPConfig struct {
	// Servers — the configured servers. Each needs exactly one of command
	// (a subprocess on this box) or url (a streamable-HTTP endpoint).
	Servers []MCPServerConfig `json:"servers,omitempty"`

	// Timeout — per-call budget for every server that does not set its own.
	// 0 ⇒ mcp.DefaultTimeout (15s). A tool slower than this is not usable in a
	// spoken turn.
	Timeout Duration `json:"timeout,omitempty"`

	// AllowHosts / DenyHosts — the host lists for the shared webfetch door that
	// url servers go through. Deny wins. Private addresses are refused
	// unconditionally unless the individual server sets allow_private.
	AllowHosts []string `json:"allow_hosts,omitempty"`
	DenyHosts  []string `json:"deny_hosts,omitempty"`

	// MaxBytes — cap on one JSON-RPC response. 0 ⇒ webfetch.DefaultMaxBytes.
	MaxBytes int64 `json:"max_bytes,omitempty"`

	// HostInterval — minimum spacing between two requests to one MCP server.
	// 0 ⇒ DefaultMCPHostInterval (50ms), NOT webfetch's own one-second default.
	// That default was sized for a feed poll loop, and this path is in a spoken
	// turn: one dial is three requests (initialize, initialized, tools/list),
	// so a second of spacing is two seconds of pure sleeping per dial and up to
	// another second before every tools/call leaves the box.
	HostInterval Duration `json:"host_interval,omitempty"`
}

// DefaultMCPHostInterval — see MCPConfig.HostInterval. Enough to stop a
// runaway loop hammering a server, small enough not to be heard.
const DefaultMCPHostInterval = 50 * time.Millisecond

// normaliseMCP applies the block's defaults. A block with no server at all is
// the same as no block. A block whose servers are all disabled is NOT
// normalised away, because validate has to see their shape — a dark block with
// a typo in it should fail at startup, which is the whole reason it can be
// written before it is switched on. wireMCP builds nothing when nothing is
// enabled, so "off" still holds.
func (c *Config) normaliseMCP() {
	if c.MCP != nil && len(c.MCP.Servers) == 0 {
		c.MCP = nil
	}
	if c.MCP != nil && c.MCP.HostInterval <= 0 {
		c.MCP.HostInterval = Duration(DefaultMCPHostInterval)
	}
}

// MCPServerConfig — one MCP server.
type MCPServerConfig struct {
	// Name — the local handle. It prefixes every tool this server contributes
	// ("vikunja" + "list_tasks" ⇒ the allowlist row "vikunja_list_tasks") and
	// becomes the store scope "mcp:<name>", so its provenance is readable on
	// /tools without opening the config.
	Name string `json:"name"`

	// Command / Args / Env / Dir — a stdio server: a child process of mavend,
	// on this box, under this user. argv, never a shell string.
	Command string   `json:"command,omitempty"`
	Args    []string `json:"args,omitempty"`
	Env     []string `json:"env,omitempty"`
	Dir     string   `json:"dir,omitempty"`

	// URL — a streamable-HTTP endpoint. It is fetched through
	// internal/webfetch, so the SSRF guard, the redirect cap, the size cap and
	// the one-request-per-host-per-second limit all apply.
	URL string `json:"url,omitempty"`

	// AllowPrivate — let THIS server be a loopback or LAN address. The Vikunja
	// server on homesrv is "http://localhost:9100/mcp", which is refused
	// without this flag. Understand what it means before setting it: a local
	// server is a DIFFERENT trust level from a public one. It is inside the
	// network, it usually needs no credential, and it can change things that
	// matter — so an argument the router got wrong lands somewhere real. Set it
	// only for a server you run yourself, and prefer allow_tools with it.
	AllowPrivate bool `json:"allow_private,omitempty"`

	// AllowTools — when set, the ONLY remote tool names taken from this server.
	// This is the knob that keeps the catalogue deliberate: the resident model
	// is a 1.7B with a 4096-token context, and a tool name it half-remembers is
	// a wrong act, so fewer and better-chosen beats complete.
	AllowTools []string `json:"allow_tools,omitempty"`

	// MaxTools — cap on this server's contribution. 0 ⇒ mcp.DefaultMaxTools (12).
	MaxTools int `json:"max_tools,omitempty"`

	// Timeout — per-call budget for this server. 0 ⇒ MCPConfig.Timeout.
	Timeout Duration `json:"timeout,omitempty"`

	// Headers — sent verbatim on every request to a url server. This is how a
	// bearer token reaches a real remote MCP server: {"Authorization": "Bearer
	// ${MCP_TOKEN}"}, with the value in the gitignored env file like the
	// telegram credentials. The Vikunja server on homesrv needs none only
	// because it is unauthenticated on loopback.
	Headers map[string]string `json:"headers,omitempty"`

	// Enabled — false (the default) keeps a configured server described but
	// dark, so a block can be written and reviewed before it is switched on.
	Enabled bool `json:"enabled,omitempty"`
}

// MCPServers maps the config blocks onto the mcp package's own type. It lives
// here so config validation and daemon wiring cannot drift on the mapping.
// Returns nil when nothing is configured or nothing is enabled.
//
// Disabled servers are dropped here, which is why validation does NOT use this
// list — see allMCPServers.
func (c *Config) MCPServers() []mcp.ServerConfig {
	return c.mcpServers(true)
}

// allMCPServers is every configured server, enabled or not, for validation.
//
// Validating only the enabled ones meant a block with both command and url, or
// a bare hostname as the url, passed startup validation while it was dark. The
// doc on Enabled says a block can be written and reviewed before it is switched
// on; the review the config layer could give was the one thing skipped. Enabled
// gates the dialing, not the shape check.
func (c *Config) allMCPServers() []mcp.ServerConfig {
	return c.mcpServers(false)
}

func (c *Config) mcpServers(onlyEnabled bool) []mcp.ServerConfig {
	if c.MCP == nil {
		return nil
	}
	out := make([]mcp.ServerConfig, 0, len(c.MCP.Servers))
	for _, s := range c.MCP.Servers {
		if onlyEnabled && !s.Enabled {
			continue
		}
		timeout := time.Duration(s.Timeout)
		if timeout <= 0 {
			timeout = time.Duration(c.MCP.Timeout)
		}
		out = append(out, mcp.ServerConfig{
			Name:         s.Name,
			Command:      s.Command,
			Args:         s.Args,
			Env:          s.Env,
			Dir:          s.Dir,
			URL:          s.URL,
			AllowPrivate: s.AllowPrivate,
			AllowTools:   s.AllowTools,
			MaxTools:     s.MaxTools,
			Headers:      s.Headers,
			Timeout:      timeout,
			Enabled:      s.Enabled,
		})
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

// validateMCP fails a block with a typo (no name, both command and url, a bare
// hostname as the url) at startup, rather than at the first turn that needed
// the tool.
func (c *Config) validateMCP() error {
	return mcp.Validate(c.allMCPServers())
}
