// This file implements plugin discovery and lifecycle management (US-016, #132):
// finding plugin executables under a config directory, loading each, and
// aggregating their tools. Loading is fault-tolerant — one plugin that fails to
// start or handshake is logged and skipped so the rest still load.
package plugin

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"

	"github.com/smallnest/pigo/internal/agentcore"
)

// Manager owns a set of loaded plugins and their aggregated tools. It is not safe
// for concurrent modification; load once at startup, then read.
type Manager struct {
	plugins []*Plugin
}

// Discover finds and loads every plugin under dir. A plugin is any executable
// regular file directly inside dir (non-executable files and subdirectories are
// ignored). Each plugin is launched and handshaked; a failure is written to
// warnLog (when non-nil) and that plugin is skipped. A missing dir is not an
// error — it yields an empty Manager. pluginStderr, when non-nil, receives every
// plugin's stderr.
func Discover(dir string, warnLog, pluginStderr io.Writer) (*Manager, error) {
	m := &Manager{}
	entries, err := os.ReadDir(dir)
	if err != nil {
		if os.IsNotExist(err) {
			return m, nil // no plugins directory → no plugins
		}
		return nil, fmt.Errorf("plugin: read dir %q: %w", dir, err)
	}
	// Deterministic load order for stable tool ordering and diagnostics.
	sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() })

	for _, e := range entries {
		if e.IsDir() {
			continue
		}
		info, err := e.Info()
		if err != nil || !isExecutable(info.Mode()) {
			continue
		}
		path := filepath.Join(dir, e.Name())
		p, err := Load(path, nil, pluginStderr)
		if err != nil {
			if warnLog != nil {
				fmt.Fprintf(warnLog, "pigo: plugin %q failed to load: %v\n", e.Name(), err)
			}
			continue
		}
		m.plugins = append(m.plugins, p)
	}
	return m, nil
}

// isExecutable reports whether the file mode has any execute bit set.
func isExecutable(mode os.FileMode) bool {
	return mode&0o111 != 0
}

// Tools returns the aggregated tools of every loaded plugin, in load order.
func (m *Manager) Tools() []agentcore.AgentTool {
	var out []agentcore.AgentTool
	for _, p := range m.plugins {
		out = append(out, p.Tools()...)
	}
	return out
}

// Plugins returns the loaded plugins (for command aggregation and diagnostics).
func (m *Manager) Plugins() []*Plugin { return m.plugins }

// PluginCommand pairs a plugin-declared slash command with the plugin that owns
// it, so a caller can dispatch the command back to its plugin via
// Plugin.CallCommand.
type PluginCommand struct {
	// Plugin is the plugin that declared and handles this command.
	Plugin *Plugin
	// Spec is the command's declaration from the owning plugin's manifest.
	Spec CommandSpec
}

// Commands returns the aggregated slash commands of every loaded plugin, in load
// order (and, within a plugin, in manifest order). Each carries the owning
// plugin so the caller can dispatch it via Plugin.CallCommand.
func (m *Manager) Commands() []PluginCommand {
	var out []PluginCommand
	for _, p := range m.plugins {
		for _, spec := range p.Manifest.Commands {
			out = append(out, PluginCommand{Plugin: p, Spec: spec})
		}
	}
	return out
}

// Subscribers reports whether any loaded plugin subscribes to the given event
// type. It lets a caller skip building an event payload when nobody is listening
// (US-017, #133).
func (m *Manager) Subscribers(eventType string) bool {
	for _, p := range m.plugins {
		if p.Subscribes(eventType) {
			return true
		}
	}
	return false
}

// DispatchEvent delivers one lifecycle event to every plugin subscribed to its
// type (US-017, #133). Delivery is best-effort and isolated: each plugin's send
// is bounded by eventTimeout, and a delivery failure to one plugin (timeout,
// dead process) is written to warnLog when non-nil and does not stop delivery to
// the others. It never blocks the agent loop beyond the per-plugin timeout.
func (m *Manager) DispatchEvent(params EventParams, warnLog io.Writer) {
	for _, p := range m.plugins {
		if !p.Subscribes(params.Type) {
			continue
		}
		if err := p.SendEvent(params); err != nil && warnLog != nil {
			fmt.Fprintf(warnLog, "pigo: plugin %q event %q: %v\n", p.Manifest.Name, params.Type, err)
		}
	}
}

// Close shuts down every loaded plugin, returning the first error encountered
// (all plugins are attempted regardless).
func (m *Manager) Close() error {
	var firstErr error
	for _, p := range m.plugins {
		if err := p.Close(); err != nil && firstErr == nil {
			firstErr = err
		}
	}
	return firstErr
}
