package dispatcher

import (
	"context"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/tidbcloud/AgentTeam/desktop/internal/agent"
	"github.com/tidbcloud/AgentTeam/desktop/internal/i18n"
	"github.com/tidbcloud/AgentTeam/desktop/internal/profile"
	"github.com/tidbcloud/AgentTeam/desktop/internal/types"
)

// InstructionHandler processes slash-prefixed command messages from channel.
// Per the architecture, all messages starting with "/" are routed here
// before reaching the Dispatcher's message dispatch path.
type InstructionHandler struct {
	dispatcher     *Dispatcher
	agentOverrides *AgentOverrides
}

// NewInstructionHandler creates a new instruction handler with access to the
// dispatcher and the shared agent overrides.
func NewInstructionHandler(dispatcher *Dispatcher, overrides *AgentOverrides) *InstructionHandler {
	return &InstructionHandler{
		dispatcher:     dispatcher,
		agentOverrides: overrides,
	}
}

// Handle attempts to process an inbound session as a slash command instruction.
// It returns true if the session was recognized and handled, false otherwise.
func (h *InstructionHandler) Handle(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	prompt := strings.TrimSpace(sess.Input.Prompt)
	if !strings.HasPrefix(prompt, "/") {
		return false, nil
	}

	command, args := splitSlashCommand(prompt)

	// Global commands (work on any channel)
	switch command {
	case "/info":
		return h.handleInfo(ctx, ch, sess)
	case "/agent":
		return h.handleAgentSwitch(ctx, ch, sess, args)
	case "/list":
		return h.listAgents(ctx, ch, sess)
	case "/new":
		return h.handleNew(ctx, ch, sess)
	case "/status":
		return h.handleStatus(ctx, ch, sess)
	case "/help":
		return h.handleHelp(ctx, ch, sess)
	case "/echo":
		return h.handleEcho(ctx, ch, sess, args)
	case "/toggle-debug":
		return h.handleToggleDebug(ctx, ch, sess)
	case "/sendmedia":
		return h.handleSendMedia(ctx, ch, sess, args)
	case "/reset":
		return h.handleReset(ctx, ch, sess)
	case "/act":
		return h.handleAct(ctx, ch, sess)
	}

	return false, nil
}

// ---------------------------------------------------------------------------
// Global commands
// ---------------------------------------------------------------------------

// handleInfo processes /info — shows current channel info and agent info.
func (h *InstructionHandler) handleInfo(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	mgr := profile.Default()
	channelID, convKey := conversationInfo(sess)

	var sb strings.Builder
	sb.WriteString(i18n.Get("cmd.info.server.header", ch.ID()))
	if convKey != "" {
		sb.WriteString(i18n.Get("cmd.info.server.conv", convKey))
	}
	if chCfg, ok := mgr.Servers().Get(channelID); ok {
		if name := strings.TrimSpace(chCfg.Name); name != "" {
			sb.WriteString(i18n.Get("cmd.info.server.name", name))
		}
	}

	agentID := h.resolveCurrentAgentID(channelID, convKey)
	if agentID == "" {
		sb.WriteString("\n" + i18n.Get("cmd.agent.current.empty"))
		return true, reply(ctx, ch, sess, sb.String())
	}

	ag, ok := mgr.Agents().Get(agentID)
	if !ok {
		sb.WriteString("\n" + i18n.Get("cmd.agent.current.not_found", agentID))
		return true, reply(ctx, ch, sess, sb.String())
	}

	sb.WriteString("\n" + i18n.Get("cmd.info.agent.header", ag.Name, ag.ID))
	sb.WriteString(i18n.Get("cmd.info.agent.runtime", ag.Runtime))
	if model := strings.TrimSpace(ag.Model); model != "" {
		sb.WriteString(i18n.Get("cmd.info.agent.model", model))
	}
	if desc := strings.TrimSpace(ag.Description); desc != "" {
		sb.WriteString("\n" + desc)
	}
	return true, reply(ctx, ch, sess, sb.String())
}

// handleAgentSwitch processes /agent <name|index> — switches agent, or shows help if no args.
func (h *InstructionHandler) handleAgentSwitch(ctx context.Context, ch types.ServerConnection, sess *types.Message, args string) (bool, error) {
	args = strings.TrimSpace(args)
	if args == "" {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.help"))
	}

	mgr := profile.Default()
	agents := mgr.Agents().All()

	idx, err := strconv.Atoi(args)
	if err == nil {
		if idx < 1 || idx > len(agents) {
			return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.switch.index_out_of_range", idx, len(agents)))
		}
		args = agents[idx-1].ID
	}

	return h.doSwitchAgent(ctx, ch, sess, args, agents)
}

func (h *InstructionHandler) doSwitchAgent(ctx context.Context, ch types.ServerConnection, sess *types.Message, name string, agents []agent.Config) (bool, error) {
	var target *agent.Config
	for i := range agents {
		if strings.EqualFold(agents[i].ID, name) || strings.EqualFold(agents[i].Name, name) {
			a := agents[i]
			target = &a
			break
		}
	}

	if target == nil {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.switch.not_found", name))
	}

	channelID, convKey := conversationInfo(sess)
	if convKey != "" {
		h.setAgentOverride(channelID, convKey, target.ID)
	}

	var sb strings.Builder
	sb.WriteString(i18n.Get("cmd.agent.switch.ok", target.Name, target.ID))
	sb.WriteString(i18n.Get("cmd.info.agent.header", target.Name, target.ID))
	sb.WriteString(i18n.Get("cmd.info.agent.runtime", target.Runtime))
	if model := strings.TrimSpace(target.Model); model != "" {
		sb.WriteString(i18n.Get("cmd.info.agent.model", model))
	}
	if desc := strings.TrimSpace(target.Description); desc != "" {
		sb.WriteString("\n" + desc)
	}
	return true, reply(ctx, ch, sess, sb.String())
}

func (h *InstructionHandler) listAgents(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	agents := profile.Default().Agents().All()
	if len(agents) == 0 {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.list.empty"))
	}

	sorted := make([]agent.Config, len(agents))
	copy(sorted, agents)
	sort.Slice(sorted, func(i, j int) bool {
		return strings.ToLower(sorted[i].ID) < strings.ToLower(sorted[j].ID)
	})

	var sb strings.Builder
	sb.WriteString(i18n.Get("cmd.agent.list.header"))
	for i, ag := range sorted {
		sb.WriteString(i18n.Get("cmd.agent.list.item", i+1, ag.Name, ag.ID, ag.Runtime))
		if desc := strings.TrimSpace(ag.Description); desc != "" {
			sb.WriteString("\n" + i18n.Get("cmd.agent.list.item.desc", desc))
		}
		sb.WriteString("\n")
	}
	return true, reply(ctx, ch, sess, strings.TrimSpace(sb.String()))
}

// handleNew stops the current interactive session so the next message starts fresh.
func (h *InstructionHandler) handleNew(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	channelID, convKey := conversationInfo(sess)
	agentID := h.resolveCurrentAgentID(channelID, convKey)

	stopped := false
	if h.dispatcher != nil && agentID != "" {
		stopped = h.dispatcher.StopByAgent(agentID)
	}
	if stopped {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.new.ok"))
	}
	return true, reply(ctx, ch, sess, i18n.Get("cmd.new.idle"))
}

// handleReset stops the current session and clears the agent workspace
// so the next message starts with a completely fresh environment.
func (h *InstructionHandler) handleReset(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	channelID, convKey := conversationInfo(sess)

	if h.dispatcher == nil {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.reset.no_dispatcher"))
	}

	agentID := h.resolveCurrentAgentID(channelID, convKey)
	if agentID == "" {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.current.empty"))
	}

	reset, err := h.dispatcher.ResetByAgent(agentID)
	if err != nil {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.reset.error", err.Error()))
	}

	if reset {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.reset.ok"))
	}
	return true, reply(ctx, ch, sess, i18n.Get("cmd.reset.idle"))
}

// handleStatus shows the current agent's work status.
func (h *InstructionHandler) handleStatus(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	mgr := profile.Default()
	channelID, convKey := conversationInfo(sess)

	agentID := h.resolveCurrentAgentID(channelID, convKey)

	var sb strings.Builder
	if agentID != "" {
		ag, ok := mgr.Agents().Get(agentID)
		if ok {
			sb.WriteString(i18n.Get("cmd.status.agent", ag.Name, ag.ID))
			sb.WriteString(i18n.Get("cmd.status.runtime", ag.Runtime))
			if model := strings.TrimSpace(ag.Model); model != "" {
				sb.WriteString(i18n.Get("cmd.status.model", model))
			}
		}
	} else {
		sb.WriteString(i18n.Get("cmd.status.unbound"))
	}

	if h.dispatcher != nil && agentID != "" {
		snap, ok := h.dispatcher.Session(agentID)
		if ok {
			sb.WriteString(i18n.Get("cmd.status.running"))
			sb.WriteString(i18n.Get("cmd.status.started_at", snap.StartedAt.Format("2006-01-02 15:04:05")))
			sb.WriteString(i18n.Get("cmd.status.uptime", formatDuration(time.Since(snap.StartedAt))))
			sb.WriteString(i18n.Get("cmd.status.workdir", snap.WorkDir))
			sb.WriteString(i18n.Get("cmd.status.session_id", snap.AgentID))
		} else {
			sb.WriteString(i18n.Get("cmd.status.idle"))
		}
	} else {
		sb.WriteString(i18n.Get("cmd.status.unknown"))
	}

	return true, reply(ctx, ch, sess, sb.String())
}

// handleAct processes /act — shows the latest 5 activity entries for the
// current agent.
func (h *InstructionHandler) handleAct(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	channelID, convKey := conversationInfo(sess)
	agentID := h.resolveCurrentAgentID(channelID, convKey)

	if agentID == "" {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.agent.current.empty"))
	}

	if h.dispatcher == nil || h.dispatcher.trajectoryHub == nil {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.act.no_data"))
	}

	items := h.dispatcher.trajectoryHub.Recent(agentID, 5)
	if len(items) == 0 {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.act.empty"))
	}

	var sb strings.Builder
	sb.WriteString(i18n.Get("cmd.act.header"))
	for i, item := range items {
		ts := time.UnixMilli(item.Timestamp).Format("15:04:05")
		e := item.Entry
		var detail string
		if e.Detail != "" {
			detail = e.Detail
		} else if e.ToolName != "" {
			detail = e.ToolName
		} else if e.Text != "" {
			detail = e.Text
		} else {
			detail = e.Activity
		}
		sb.WriteString(i18n.Get("cmd.act.item", i+1, ts, e.Activity, detail))
	}
	return true, reply(ctx, ch, sess, sb.String())
}

// handleHelp shows the help text.
func (h *InstructionHandler) handleHelp(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	helpText := i18n.Get("cmd.help.global")
	return true, reply(ctx, ch, sess, helpText)
}

// handleEcho echoes the args back and appends a timing report.
func (h *InstructionHandler) handleEcho(ctx context.Context, ch types.ServerConnection, sess *types.Message, args string) (bool, error) {
	if message := strings.TrimSpace(args); message != "" {
		if err := reply(ctx, ch, sess, message); err != nil {
			return true, err
		}
	}
	return true, reply(ctx, ch, sess, buildTimingReport(sess.CreatedAt(), sess.Input.Meta))
}

// handleToggleDebug toggles debug mode on channels that support it.
func (h *InstructionHandler) handleToggleDebug(ctx context.Context, ch types.ServerConnection, sess *types.Message) (bool, error) {
	toggler, ok := ch.(types.DebugToggler)
	if !ok {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.debug.unsupported"))
	}
	enabled := toggler.ToggleDebug()
	r := i18n.Get("cmd.debug.off")
	if enabled {
		r = i18n.Get("cmd.debug.on")
	}
	return true, reply(ctx, ch, sess, r)
}

// handleSendMedia sends a media file using the channel's Send method.
func (h *InstructionHandler) handleSendMedia(ctx context.Context, ch types.ServerConnection, sess *types.Message, args string) (bool, error) {
	mediaSource, caption, err := consumeCommandArg(strings.TrimSpace(args))
	if err != nil {
		return true, reply(ctx, ch, sess, i18n.Get("cmd.sendmedia.usage"))
	}

	if err := ch.Send(ctx, sess.Input.Meta, types.OutboundMessage{
		Content:     caption,
		Attachments: []types.OutboundAttachment{{Path: mediaSource}},
	}); err != nil {
		notice := i18n.Get("cmd.sendmedia.error", previewCommandError(err))
		_ = reply(ctx, ch, sess, notice)
		return true, err
	}
	return true, nil
}

// ---------------------------------------------------------------------------
// Agent override helpers
// ---------------------------------------------------------------------------

func (h *InstructionHandler) resolveCurrentAgentID(channelID, convKey string) string {
	mgr := profile.Default()
	agentID := ""
	if convKey != "" {
		agentID = h.getAgentOverride(channelID, convKey)
	}
	if agentID == "" {
		if chCfg, ok := mgr.Servers().Get(channelID); ok {
			agentID = strings.TrimSpace(chCfg.AgentID)
		}
	}
	if agentID == "" {
		all := mgr.Agents().All()
		if len(all) == 1 {
			agentID = all[0].ID
		}
	}
	return agentID
}

func (h *InstructionHandler) getAgentOverride(channelID, convKey string) string {
	return h.agentOverrides.Get(channelID, convKey)
}

func (h *InstructionHandler) setAgentOverride(channelID, convKey, agentID string) {
	h.agentOverrides.Set(channelID, convKey, agentID)
}

// ---------------------------------------------------------------------------
// Channel helpers
// ---------------------------------------------------------------------------

// reply sends a text message to the channel that originated the session.
func reply(ctx context.Context, ch types.ServerConnection, sess *types.Message, text string) error {
	err := ch.Send(ctx, sess.Input.Meta, types.OutboundMessage{Content: text})
	return err
}

func conversationInfo(sess *types.Message) (channelID, conversationKey string) {
	channelID = strings.TrimSpace(sess.Input.Channel)
	if sess.Input.Meta != nil {
		conversationKey = conversationKeyForMetadata(sess.Input.Meta)
	}
	return
}

// ---------------------------------------------------------------------------
// Command utilities
// ---------------------------------------------------------------------------

// buildTimingReport generates a timing report comparing the message event
// timestamp with the time the instruction was received and processed.
func buildTimingReport(receivedAt time.Time, meta types.Metadata) string {
	eventText := "N/A"
	platformDelay := "N/A"
	normalized := types.NormalizeMetadata(meta)
	if normalized.HasEventTime() {
		eventTime := normalized.EventTime
		eventText = eventTime.UTC().Format(time.RFC3339)
		delay := receivedAt.Sub(eventTime)
		if delay < 0 {
			delay = 0
		}
		platformDelay = fmt.Sprintf("%dms", delay.Milliseconds())
	}
	processDelay := time.Since(receivedAt)
	if processDelay < 0 {
		processDelay = 0
	}
	return strings.Join([]string{
		i18n.Get("cmd.timing.header"),
		i18n.Get("cmd.timing.event", eventText),
		i18n.Get("cmd.timing.platform", platformDelay),
		i18n.Get("cmd.timing.process", processDelay.Milliseconds()),
	}, "\n")
}

func consumeCommandArg(input string) (string, string, error) {
	trimmed := strings.TrimSpace(input)
	if trimmed == "" {
		return "", "", fmt.Errorf("missing argument")
	}
	if trimmed[0] == '"' || trimmed[0] == '\'' {
		quote := trimmed[0]
		for i := 1; i < len(trimmed); i++ {
			if trimmed[i] != quote || trimmed[i-1] == '\\' {
				continue
			}
			value, err := strconv.Unquote(trimmed[:i+1])
			if err != nil {
				return "", "", fmt.Errorf("parse quoted argument: %w", err)
			}
			return value, strings.TrimSpace(trimmed[i+1:]), nil
		}
		return "", "", fmt.Errorf("unterminated quoted argument")
	}
	for i, r := range trimmed {
		if r == ' ' || r == '\t' || r == '\r' || r == '\n' {
			return trimmed[:i], strings.TrimSpace(trimmed[i+1:]), nil
		}
	}
	return trimmed, "", nil
}

func previewCommandError(err error) string {
	message := strings.TrimSpace(err.Error())
	if len([]rune(message)) > 200 {
		return string([]rune(message)[:200]) + "..."
	}
	return message
}

// splitSlashCommand splits a slash command into the command name and arguments.
func splitSlashCommand(prompt string) (string, string) {
	trimmed := strings.TrimSpace(prompt)
	if trimmed == "" {
		return "", ""
	}
	if index := strings.IndexAny(trimmed, " \t\r\n"); index >= 0 {
		return strings.ToLower(trimmed[:index]), strings.TrimSpace(trimmed[index+1:])
	}
	return strings.ToLower(trimmed), ""
}

// formatDuration formats a duration as a human-readable string like "1h23m45s".
func formatDuration(d time.Duration) string {
	d = d.Round(time.Second)
	h := d / time.Hour
	d -= h * time.Hour
	m := d / time.Minute
	d -= m * time.Minute
	s := d / time.Second

	switch {
	case h > 0:
		return fmt.Sprintf("%dh%dm%ds", h, m, s)
	case m > 0:
		return fmt.Sprintf("%dm%ds", m, s)
	default:
		return fmt.Sprintf("%ds", s)
	}
}
