package devagents

import (
	"context"
	"fmt"
	"io"
	"log/slog"
	"os"
	"path/filepath"
	"strings"
	"time"

	"maunium.net/go/mautrix/event"

	"github.com/enmanuel/agents/internal/config"
	"github.com/enmanuel/agents/pkg/command"
	"github.com/enmanuel/agents/pkg/decision"
	"github.com/enmanuel/agents/shell/matrix"
)

// Robot is a lightweight runtime for command-only bots.
// Unlike Agent, it has no LLM, rules, memory, knowledge, skills, or tools.
// It connects to Matrix and dispatches commands; non-command messages are ignored.
type Robot struct {
	cfg    *config.AgentConfig
	matrix *matrix.Client
	logger *slog.Logger

	// E2EE crypto store — non-nil when encryption is enabled; closed on shutdown.
	cryptoStore io.Closer

	// Lifecycle
	cancel context.CancelFunc
	done   chan struct{}

	// Commands — handlers keyed by canonical name; aliases maps alias → canonical.
	commands    map[string]CommandHandler
	cmdAliases  map[string]string
	customSpecs []command.Spec
	startTime   time.Time

	// Personality prefix for replies
	prefix string

	// Matrix listener
	listener *matrix.Listener
}

// NewRobot creates a lightweight command-only bot from its config and logger.
// It initializes only the Matrix client, E2EE (if configured), and built-in commands.
func NewRobot(cfg *config.AgentConfig, logger *slog.Logger) (*Robot, error) {
	matrixClient, err := matrix.New(cfg.Matrix)
	if err != nil {
		return nil, fmt.Errorf("matrix client: %w", err)
	}

	// E2EE — initialize before the sync loop starts
	var cryptoStore io.Closer
	if cfg.Matrix.Encryption.Enabled {
		storePath := filepath.Join(cfg.Matrix.Encryption.StorePath, "crypto.db")
		pickleKey := os.Getenv(cfg.Matrix.Encryption.PickleKeyEnv)
		logger.Info("initializing e2ee", "store", storePath)
		cryptoStore, err = matrixClient.InitCrypto(context.Background(), storePath, pickleKey, cfg.Agent.ID)
		if err != nil {
			return nil, fmt.Errorf("e2ee init: %w", err)
		}

		// Auto-fetch cross-signing private keys from SSSS if recovery key is configured.
		if envName := cfg.Matrix.Encryption.RecoveryKeyEnv; envName != "" {
			if rk := os.Getenv(envName); rk != "" {
				if err := matrixClient.FetchCrossSigningKeys(context.Background(), rk); err != nil {
					logger.Warn("failed to fetch cross-signing keys from SSSS (non-fatal)", "err", err)
				} else {
					logger.Info("cross-signing private keys fetched from SSSS")
				}
			}
		}

		// Sign own device with the self-signing key so Element shows it as verified.
		if err := matrixClient.SignOwnDevice(context.Background()); err != nil {
			logger.Warn("failed to sign own device (non-fatal)", "err", err)
		} else {
			logger.Info("own device signed with cross-signing key")
		}

		logger.Info("e2ee ready")
	}

	r := &Robot{
		cfg:         cfg,
		matrix:      matrixClient,
		logger:      logger,
		cryptoStore: cryptoStore,
		done:        make(chan struct{}),
		commands:    make(map[string]CommandHandler),
		cmdAliases:  command.BuiltinNames(),
		startTime:   time.Now(),
		prefix:      cfg.Personality.Prefix,
	}

	// Register built-in commands (robot-appropriate subset).
	r.registerBuiltinCommands()

	// Matrix event listener
	r.listener = matrix.NewListener(matrixClient, cfg.Matrix, r.handleEvent, logger)

	return r, nil
}

// registerBuiltinCommands registers command handlers appropriate for a robot.
// Robots support: help, ping, status, info, version.
// They do NOT support: tools, tool, clear, prompts (no LLM, no memory, no tools).
func (r *Robot) registerBuiltinCommands() {
	r.commands["help"] = r.cmdHelp
	r.commands["ping"] = r.cmdPing
	r.commands["status"] = r.cmdStatus
	r.commands["info"] = r.cmdInfo
	r.commands["version"] = r.cmdVersion
}

// RegisterCommand adds a custom command handler for this robot.
func (r *Robot) RegisterCommand(spec command.Spec, handler CommandHandler) {
	r.commands[spec.Name] = handler
	r.cmdAliases[spec.Name] = spec.Name
	for _, alias := range spec.Aliases {
		r.cmdAliases[alias] = spec.Name
	}
	r.customSpecs = append(r.customSpecs, spec)
	r.logger.Info("command_registered", "command", spec.Name, "aliases", spec.Aliases)
}

// Run starts the robot sync loop. Blocks until ctx is cancelled.
func (r *Robot) Run(ctx context.Context) error {
	ctx, r.cancel = context.WithCancel(ctx)
	defer close(r.done)

	if r.cryptoStore != nil {
		defer r.cryptoStore.Close()
	}

	r.logger.Info("robot starting",
		"id", r.cfg.Agent.ID,
		"name", r.cfg.Agent.Name,
		"type", "robot",
	)

	// Set presence to online
	if err := r.matrix.SetPresence(ctx, event.PresenceOnline); err != nil {
		r.logger.Warn("failed to set presence online", "err", err)
	}
	defer func() {
		offlineCtx := context.Background()
		if err := r.matrix.SetPresence(offlineCtx, event.PresenceOffline); err != nil {
			r.logger.Warn("failed to set presence offline", "err", err)
		}
	}()

	return r.listener.Run(ctx)
}

// Stop cancels this robot's individual context, causing Run to return.
func (r *Robot) Stop() {
	if r.cancel != nil {
		r.cancel()
	}
}

// Done returns a channel that is closed when Run has returned.
func (r *Robot) Done() <-chan struct{} {
	return r.done
}

// handleEvent is called by the matrix Listener for each filtered incoming event.
// For a robot, only commands are processed; all other messages are silently ignored.
func (r *Robot) handleEvent(ctx context.Context, msgCtx decision.MessageContext, evt *event.Event) {
	roomID := evt.RoomID.String()

	// Only process commands. Non-command messages are silently ignored.
	if msgCtx.Command == "" {
		r.logger.Debug("non-command message, ignoring (robot)",
			"sender", msgCtx.SenderID,
			"room", roomID,
		)
		return
	}

	r.logger.Info("command_received",
		"command", msgCtx.Command,
		"sender", msgCtx.SenderID,
		"room", roomID,
		"args", msgCtx.Args,
	)

	// Resolve aliases
	cmdName := msgCtx.Command
	if canonical, ok := r.cmdAliases[cmdName]; ok {
		cmdName = canonical
	}

	if handler, ok := r.commands[cmdName]; ok {
		r.logger.Info("command_executed", "command", cmdName)
		reply := handler(ctx, msgCtx)
		_ = r.sendReply(ctx, roomID, msgCtx.EventID, msgCtx.ThreadID, reply)
		return
	}

	// Unknown command
	r.logger.Info("command_unknown", "command", msgCtx.Command)
	unknownMsg := fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command)
	if r.cfg.Matrix.Filters.CommandPrefix == "" {
		unknownMsg = fmt.Sprintf("Comando desconocido: `%s`. Usa `help` para ver comandos disponibles.", msgCtx.Command)
	}
	_ = r.sendReply(ctx, roomID, msgCtx.EventID, msgCtx.ThreadID, unknownMsg)
}

// sendReply sends a markdown reply that respects thread context.
func (r *Robot) sendReply(ctx context.Context, roomID, eventID, threadID, markdown string) error {
	if threadID != "" {
		return r.matrix.SendThreadMarkdown(ctx, roomID, threadID, eventID, markdown)
	}
	return r.matrix.SendReplyMarkdown(ctx, roomID, eventID, markdown)
}

// ── Built-in command handlers (robot subset) ─────────────────────────────

func (r *Robot) cmdHelp(_ context.Context, _ decision.MessageContext) string {
	var b strings.Builder
	b.WriteString("**Comandos disponibles:**\n\n")

	prefix := r.cfg.Matrix.Filters.CommandPrefix // "!" or ""

	// Built-in commands appropriate for robots
	robotBuiltins := []command.Spec{
		{Name: "help", Aliases: []string{"h"}, Description: "Lista comandos disponibles", Usage: prefix + "help"},
		{Name: "ping", Description: "Alive check", Usage: prefix + "ping"},
		{Name: "status", Description: "Info del robot: uptime", Usage: prefix + "status"},
		{Name: "info", Description: "Nombre, version y descripcion", Usage: prefix + "info"},
		{Name: "version", Aliases: []string{"v"}, Description: "Version del robot", Usage: prefix + "version"},
	}
	for _, spec := range robotBuiltins {
		writeSpec(&b, spec)
	}

	// Agent-specific commands (registered via RegisterCommand)
	if len(r.customSpecs) > 0 {
		b.WriteString("\n**Comandos del robot:**\n\n")
		for _, spec := range r.customSpecs {
			if spec.Hidden {
				continue
			}
			writeSpec(&b, spec)
		}
	}

	return b.String()
}

func (r *Robot) cmdPing(_ context.Context, _ decision.MessageContext) string {
	return fmt.Sprintf("pong — %s", time.Now().Format(time.RFC3339))
}

func (r *Robot) cmdStatus(_ context.Context, _ decision.MessageContext) string {
	uptime := time.Since(r.startTime).Truncate(time.Second)

	var b strings.Builder
	fmt.Fprintf(&b, "**Estado de %s:**\n\n", r.cfg.Agent.Name)
	fmt.Fprintf(&b, "- **Tipo:** robot\n")
	fmt.Fprintf(&b, "- **Uptime:** %s\n", uptime)
	fmt.Fprintf(&b, "- **Comandos custom:** %d\n", len(r.customSpecs))

	return b.String()
}

func (r *Robot) cmdInfo(_ context.Context, _ decision.MessageContext) string {
	var b strings.Builder

	b.WriteString("## Identidad\n\n")
	fmt.Fprintf(&b, "- **Nombre:** %s\n", r.cfg.Agent.Name)
	fmt.Fprintf(&b, "- **ID:** `%s`\n", r.cfg.Agent.ID)
	fmt.Fprintf(&b, "- **Tipo:** robot\n")
	if r.cfg.Agent.Version != "" {
		fmt.Fprintf(&b, "- **Version:** %s\n", r.cfg.Agent.Version)
	}
	fmt.Fprintf(&b, "- **Descripcion:** %s\n", r.cfg.Agent.Description)

	uptime := time.Since(r.startTime).Round(time.Second)
	b.WriteString("\n## Uptime\n\n")
	fmt.Fprintf(&b, "- **Activo desde:** %s\n", uptime)

	return b.String()
}

func (r *Robot) cmdVersion(_ context.Context, _ decision.MessageContext) string {
	v := r.cfg.Agent.Version
	if v == "" {
		v = "sin version"
	}
	return fmt.Sprintf("%s %s", r.cfg.Agent.Name, v)
}
