// Package effects interprets pure []decision.Action values into real side effects.
package effects

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/enmanuel/agents/pkg/decision"
	"github.com/enmanuel/agents/shell/logger"
	"github.com/enmanuel/agents/shell/ssh"
)

// Result holds the outcome of executing a single action.
type Result struct {
	Action decision.Action
	Output string
	Err    error
}

// Sender is the transport-neutral message-sending capability the runner depends
// on. It is satisfied by the unibus bus sender (and was satisfied by the Matrix
// client before the bus migration). SendTyping is a no-op on transports without
// typing indicators.
type Sender interface {
	SendText(ctx context.Context, roomID, text string) error
	SendMarkdown(ctx context.Context, roomID, markdown string) error
	SendReplyMarkdown(ctx context.Context, roomID, inReplyTo, markdown string) error
	SendThreadMarkdown(ctx context.Context, roomID, threadRootID, inReplyTo, markdown string) error
	SendTyping(ctx context.Context, roomID string, typing bool) error
}

// Runner interprets actions and executes them.
type Runner struct {
	sender Sender
	ssh    *ssh.Executor
	logger *slog.Logger
}

// NewRunner creates a Runner with the provided dependencies.
func NewRunner(sender Sender, ssh *ssh.Executor, logger *slog.Logger) *Runner {
	return &Runner{sender: sender, ssh: ssh, logger: logger}
}

// Execute runs each action sequentially and returns results.
func (r *Runner) Execute(ctx context.Context, roomID string, actions []decision.Action) []Result {
	r.logger.Debug("effects_batch", "room", roomID, "count", len(actions))
	results := make([]Result, 0, len(actions))
	for _, a := range actions {
		start := time.Now()
		res := r.executeOne(ctx, roomID, a)
		ms := time.Since(start).Milliseconds()
		results = append(results, res)
		if res.Err != nil {
			r.logger.Error("action_failed", logger.FieldAction, a.Kind, logger.FieldDurationMS, ms, "err", res.Err)
		} else {
			r.logger.Info("action_done", logger.FieldAction, a.Kind, logger.FieldDurationMS, ms)
		}
	}
	return results
}

func (r *Runner) executeOne(ctx context.Context, roomID string, a decision.Action) Result {
	switch a.Kind {
	case decision.ActionKindReply:
		if a.Reply == nil {
			return Result{Action: a, Err: fmt.Errorf("nil reply action")}
		}
		var err error
		switch {
		case a.Reply.ThreadID != "":
			// Thread reply: send as part of the thread with fallback in_reply_to
			err = r.sender.SendThreadMarkdown(ctx, roomID, a.Reply.ThreadID, a.Reply.InReplyTo, a.Reply.Content)
		case a.Reply.InReplyTo != "":
			err = r.sender.SendReplyMarkdown(ctx, roomID, a.Reply.InReplyTo, a.Reply.Content)
		default:
			err = r.sender.SendMarkdown(ctx, roomID, a.Reply.Content)
		}
		return Result{Action: a, Output: a.Reply.Content, Err: err}

	case decision.ActionKindSSH:
		if a.SSH == nil {
			return Result{Action: a, Err: fmt.Errorf("nil ssh action")}
		}
		res := r.ssh.Execute(ctx, *a.SSH)
		output := res.Stdout
		if res.Stderr != "" {
			output += "\nstderr: " + res.Stderr
		}
		return Result{Action: a, Output: output, Err: res.Err}

	default:
		return Result{Action: a, Err: fmt.Errorf("unhandled action kind: %s", a.Kind)}
	}
}
