package command

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

	"github.com/memohai/memoh/internal/bots"
	"github.com/memohai/memoh/internal/browsercontexts"
	dbsqlc "github.com/memohai/memoh/internal/db/sqlc"
	emailpkg "github.com/memohai/memoh/internal/email"
	"github.com/memohai/memoh/internal/heartbeat"
	"github.com/memohai/memoh/internal/mcp"
	memprovider "github.com/memohai/memoh/internal/memory/adapters"
	"github.com/memohai/memoh/internal/models"
	"github.com/memohai/memoh/internal/providers"
	"github.com/memohai/memoh/internal/schedule"
	"github.com/memohai/memoh/internal/searchproviders"
	"github.com/memohai/memoh/internal/settings"
)

// MemberRoleResolver resolves a user's role within a bot.
type MemberRoleResolver interface {
	GetMemberRole(ctx context.Context, botID, channelIdentityID string) (string, error)
}

// BotMemberRoleAdapter adapts bots.Service to MemberRoleResolver.
type BotMemberRoleAdapter struct {
	BotService *bots.Service
}

func (a *BotMemberRoleAdapter) GetMemberRole(ctx context.Context, botID, channelIdentityID string) (string, error) {
	bot, err := a.BotService.Get(ctx, botID)
	if err != nil {
		return "", err
	}
	if bot.OwnerUserID == channelIdentityID {
		return "owner", nil
	}
	return "", nil
}

// Handler processes slash commands intercepted before they reach the LLM.
type Handler struct {
	registry        *Registry
	roleResolver    MemberRoleResolver
	scheduleService *schedule.Service
	settingsService *settings.Service
	mcpConnService  *mcp.ConnectionService

	modelsService      *models.Service
	providersService   *providers.Service
	memProvService     *memprovider.Service
	searchProvService  *searchproviders.Service
	browserCtxService  *browsercontexts.Service
	emailService       *emailpkg.Service
	emailOutboxService *emailpkg.OutboxService
	heartbeatService   *heartbeat.Service
	queries            *dbsqlc.Queries
	skillLoader        SkillLoader
	containerFS        ContainerFS

	logger *slog.Logger
}

// NewHandler creates a Handler with all required services.
func NewHandler(
	log *slog.Logger,
	roleResolver MemberRoleResolver,
	scheduleService *schedule.Service,
	settingsService *settings.Service,
	mcpConnService *mcp.ConnectionService,
	modelsService *models.Service,
	providersService *providers.Service,
	memProvService *memprovider.Service,
	searchProvService *searchproviders.Service,
	browserCtxService *browsercontexts.Service,
	emailService *emailpkg.Service,
	emailOutboxService *emailpkg.OutboxService,
	heartbeatService *heartbeat.Service,
	queries *dbsqlc.Queries,
	skillLoader SkillLoader,
	containerFS ContainerFS,
) *Handler {
	if log == nil {
		log = slog.Default()
	}
	h := &Handler{
		roleResolver:       roleResolver,
		scheduleService:    scheduleService,
		settingsService:    settingsService,
		mcpConnService:     mcpConnService,
		modelsService:      modelsService,
		providersService:   providersService,
		memProvService:     memProvService,
		searchProvService:  searchProvService,
		browserCtxService:  browserCtxService,
		emailService:       emailService,
		emailOutboxService: emailOutboxService,
		heartbeatService:   heartbeatService,
		queries:            queries,
		skillLoader:        skillLoader,
		containerFS:        containerFS,
		logger:             log.With(slog.String("component", "command")),
	}
	h.registry = h.buildRegistry()
	return h
}

// topLevelCommands are standalone commands (no sub-actions) that are
// recognised by IsCommand and listed in /help. They are handled outside
// the regular resource-group dispatch (e.g. in the channel inbound
// processor which has the required routing context).
var topLevelCommands = map[string]string{
	"new": "Start a new conversation (resets session context)",
}

// IsCommand reports whether the text contains a slash command.
// Handles both direct commands ("/help") and mention-prefixed commands ("@bot /help").
func (h *Handler) IsCommand(text string) bool {
	cmdText := ExtractCommandText(text)
	if cmdText == "" || len(cmdText) < 2 {
		return false
	}
	// Validate that it refers to a known command, not arbitrary "/path/to/file".
	parsed, err := Parse(cmdText)
	if err != nil {
		return false
	}
	if parsed.Resource == "help" {
		return true
	}
	if _, ok := topLevelCommands[parsed.Resource]; ok {
		return true
	}
	_, ok := h.registry.groups[parsed.Resource]
	return ok
}

// Execute parses and runs a slash command, returning the text reply.
func (h *Handler) Execute(ctx context.Context, botID, channelIdentityID, text string) (string, error) {
	cmdText := ExtractCommandText(text)
	if cmdText == "" {
		return h.registry.GlobalHelp(), nil
	}
	parsed, err := Parse(cmdText)
	if err != nil {
		return h.registry.GlobalHelp(), nil
	}

	// Resolve the user's role in this bot.
	role := ""
	if h.roleResolver != nil && channelIdentityID != "" {
		r, err := h.roleResolver.GetMemberRole(ctx, botID, channelIdentityID)
		if err != nil {
			h.logger.Warn("failed to resolve member role",
				slog.String("bot_id", botID),
				slog.String("channel_identity_id", channelIdentityID),
				slog.Any("error", err),
			)
		} else {
			role = r
		}
	}

	cc := CommandContext{
		Ctx:   ctx,
		BotID: botID,
		Role:  role,
		Args:  parsed.Args,
	}

	// /help
	if parsed.Resource == "help" {
		return h.registry.GlobalHelp(), nil
	}

	// Top-level commands (e.g. /new) are handled by the channel inbound
	// processor which has the required routing context. If Execute is
	// called for one of these, return a short usage hint.
	if desc, ok := topLevelCommands[parsed.Resource]; ok {
		return fmt.Sprintf("/%s - %s", parsed.Resource, desc), nil
	}

	group, ok := h.registry.groups[parsed.Resource]
	if !ok {
		return fmt.Sprintf("Unknown command: /%s\n\n%s", parsed.Resource, h.registry.GlobalHelp()), nil
	}

	if parsed.Action == "" {
		if group.DefaultAction != "" {
			parsed.Action = group.DefaultAction
		} else {
			return group.Usage(), nil
		}
	}

	sub, ok := group.commands[parsed.Action]
	if !ok {
		return fmt.Sprintf("Unknown action \"%s\" for /%s.\n\n%s", parsed.Action, parsed.Resource, group.Usage()), nil
	}

	if sub.IsWrite && role != "owner" {
		return "Permission denied: only the bot owner can execute this command.", nil
	}

	result, handlerErr := safeExecute(sub.Handler, cc)
	if handlerErr != nil {
		return fmt.Sprintf("Error: %s", handlerErr.Error()), nil
	}
	return result, nil
}

// safeExecute runs a sub-command handler and recovers from panics.
func safeExecute(fn func(CommandContext) (string, error), cc CommandContext) (result string, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("internal error: %v", r)
		}
	}()
	return fn(cc)
}
