package cmd

import (
	"fmt"
	"mime"
	"path/filepath"
	"strings"

	"github.com/nextlevelbuilder/goclaw/internal/agent"
	"github.com/nextlevelbuilder/goclaw/internal/bus"
	"github.com/nextlevelbuilder/goclaw/internal/channels"
	"github.com/nextlevelbuilder/goclaw/internal/config"
	"github.com/nextlevelbuilder/goclaw/internal/sessions"
)

// resolveAgentRoute determines which agent should handle a message
// based on config bindings. Priority: peer → channel → default.
// Matching TS resolve-route.ts binding resolution.
func resolveAgentRoute(cfg *config.Config, channel, chatID, peerKind string) string {
	for _, binding := range cfg.Bindings {
		match := binding.Match
		if match.Channel != channel {
			continue
		}

		// Peer-level match (most specific)
		if match.Peer != nil {
			if match.Peer.Kind == peerKind && match.Peer.ID == chatID {
				return config.NormalizeAgentID(binding.AgentID)
			}
			continue // has peer constraint but doesn't match — skip
		}

		// Channel-level match (least specific, no peer constraint)
		return config.NormalizeAgentID(binding.AgentID)
	}

	return cfg.ResolveDefaultAgentID()
}

// overrideSessionKeyFromLocalKey extracts topic/thread ID from the composite
// local_key and returns the correct session key for forum topics or DM threads.
// If localKey is empty or has no suffix, the original sessionKey is returned unchanged.
func overrideSessionKeyFromLocalKey(sessionKey, localKey, agentID, channel, chatID, peerKind string) string {
	if localKey == "" {
		return sessionKey
	}
	if idx := strings.Index(localKey, ":topic:"); idx > 0 && peerKind == string(sessions.PeerGroup) {
		var topicID int
		fmt.Sscanf(localKey[idx+7:], "%d", &topicID)
		if topicID > 0 {
			return sessions.BuildGroupTopicSessionKey(agentID, channel, chatID, topicID)
		}
	} else if idx := strings.Index(localKey, ":thread:"); idx > 0 && peerKind == string(sessions.PeerDirect) {
		var threadID int
		fmt.Sscanf(localKey[idx+8:], "%d", &threadID)
		if threadID > 0 {
			return sessions.BuildDMThreadSessionKey(agentID, channel, chatID, threadID)
		}
	}
	return sessionKey
}

// extractSessionMetadata builds a metadata map from channel InboundMessage metadata.
// Used to persist friendly names (display_name, username, chat_title) into sessions
// and user profiles so the web UI can show human-readable labels.
func extractSessionMetadata(msg bus.InboundMessage, peerKind string) map[string]string {
	meta := make(map[string]string)

	// Display name: prefer first_name (Telegram), fall back to display_name (Discord)
	if v := msg.Metadata["first_name"]; v != "" {
		meta["display_name"] = v
	} else if v := msg.Metadata["display_name"]; v != "" {
		meta["display_name"] = v
	}

	if v := msg.Metadata["username"]; v != "" {
		meta["username"] = v
	}
	if peerKind != "" {
		meta["peer_kind"] = peerKind
	}
	if v := msg.Metadata["chat_title"]; v != "" {
		meta["chat_title"] = v
	}

	if len(meta) == 0 {
		return nil
	}
	return meta
}

// buildAnnounceOutMeta builds outbound metadata for announce messages so that
// Send() can route replies to the correct forum topic or DM thread.
func buildAnnounceOutMeta(localKey string) map[string]string {
	if localKey == "" {
		return nil
	}
	meta := map[string]string{"local_key": localKey}
	if idx := strings.Index(localKey, ":topic:"); idx > 0 {
		meta["message_thread_id"] = localKey[idx+7:]
	} else if idx := strings.Index(localKey, ":thread:"); idx > 0 {
		meta["message_thread_id"] = localKey[idx+8:]
	}
	return meta
}

// mediaToMarkdown converts media results to markdown image/link syntax using the
// /v1/files/ HTTP endpoint. Used for WS channel where outbound media attachments
// are not supported (no channel handler). Returns empty string if no media.
// Uses absolute file paths with the /v1/files endpoint (auth-token protected).
// Generates relative URLs (/v1/files/...) so they work regardless of the server's
// external hostname — the browser resolves them from the current origin.
func mediaToMarkdown(media []agent.MediaResult, cfg *config.Config) string {
	if len(media) == 0 {
		return ""
	}

	tokenQuery := ""
	if cfg.Gateway.Token != "" {
		tokenQuery = "?token=" + cfg.Gateway.Token
	}

	var parts []string
	for _, mr := range media {
		cleanPath := filepath.Clean(mr.Path)
		// Strip leading "/" so URL path is /v1/files/app/.goclaw/...
		urlPath := strings.TrimPrefix(cleanPath, "/")
		if urlPath == "" {
			continue
		}
		fileURL := "/v1/files/" + urlPath + tokenQuery
		if strings.HasPrefix(mr.ContentType, "image/") {
			parts = append(parts, fmt.Sprintf("![image](%s)", fileURL))
		} else {
			parts = append(parts, fmt.Sprintf("[%s](%s)", filepath.Base(mr.Path), fileURL))
		}
	}
	if len(parts) == 0 {
		return ""
	}
	return "\n\n" + strings.Join(parts, "\n")
}

// mediaToMarkdownFromPaths is like mediaToMarkdown but accepts raw file paths
// ([]string from bus.InboundMessage.Media) instead of []agent.MediaResult.
func mediaToMarkdownFromPaths(files []bus.MediaFile, cfg *config.Config) string {
	if len(files) == 0 {
		return ""
	}
	media := make([]agent.MediaResult, 0, len(files))
	for _, f := range files {
		ct := f.MimeType
		if ct == "" {
			ct = mime.TypeByExtension(filepath.Ext(f.Path))
		}
		if ct == "" {
			ct = "application/octet-stream"
		}
		media = append(media, agent.MediaResult{
			Path:        f.Path,
			ContentType: ct,
		})
	}
	return mediaToMarkdown(media, cfg)
}

// resolveChannelType returns the platform type for a channel instance name.
// Returns empty string if channelMgr is nil or channel name is empty.
func resolveChannelType(channelMgr *channels.Manager, name string) string {
	if channelMgr == nil || name == "" {
		return ""
	}
	return channelMgr.ChannelTypeForName(name)
}
