package imchannel

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/sirupsen/logrus"

	"github.com/tingly-dev/tingly-box/agentboot"
	"github.com/tingly-dev/tingly-box/agentboot/ask"
	"github.com/tingly-dev/tingly-box/imbot"
)

// IMPrompter implements ask.Prompter using IM (Telegram, etc.) for user interaction
type IMPrompter struct {
	mu sync.RWMutex

	// manager is the IM bot manager for sending messages
	manager *imbot.Manager

	// registry is the tool handler registry for customizing prompts and responses
	registry *ask.ToolHandlerRegistry

	// pendingRequests stores requests waiting for user response
	// key: requestID, value: *pendingIMRequest
	pendingRequests map[string]*pendingIMRequest

	// responseChannels stores channels for sending responses back to waiting goroutines
	// key: requestID, value: channel for result
	responseChannels map[string]chan ask.Result

	// defaultTimeout is the default timeout for requests
	defaultTimeout time.Duration

	// whitelist stores tools that have been approved with "Always Allow"
	// key: toolName, value: true
	whitelist map[string]bool
}

// pendingIMRequest stores a pending request with its context
type pendingIMRequest struct {
	request        ask.Request
	chatID         string
	platform       imbot.Platform
	messageID      string
	createdAt      time.Time
	partialAnswers map[string]string // question text -> selected label (for multi-question AskUserQuestion)
	totalQuestions int               // total number of questions (set when request is created)
}

// NewIMPrompter creates a new IM-based prompter
func NewIMPrompter(manager *imbot.Manager) *IMPrompter {
	return &IMPrompter{
		manager:          manager,
		registry:         ask.NewToolHandlerRegistry(),
		pendingRequests:  make(map[string]*pendingIMRequest),
		responseChannels: make(map[string]chan ask.Result),
		defaultTimeout:   5 * time.Minute,
		whitelist:        make(map[string]bool),
	}
}

// SetDefaultTimeout sets the default timeout for requests
func (p *IMPrompter) SetDefaultTimeout(timeout time.Duration) {
	p.mu.Lock()
	defer p.mu.Unlock()
	p.defaultTimeout = timeout
}

// Prompt prompts the user via IM for response
// This implements the ask.Prompter interface
func (p *IMPrompter) Prompt(ctx context.Context, req ask.Request) (ask.Result, error) {
	// Get the chat context from the request
	chatID, platform := req.ChatID, imbot.Platform(req.Platform)
	botUUID := req.BotUUID
	if chatID == "" {
		// No chat context, auto-approve
		logrus.WithField("id", req.ID).Debug("No chat context for request, auto-approving")
		return ask.Result{
			ID:           req.ID,
			Approved:     true,
			UpdatedInput: req.Input,
		}, nil
	}

	// Check whitelist for permission requests
	if req.Type == ask.TypePermission && req.ToolName != "" {
		p.mu.RLock()
		if p.whitelist[req.ToolName] {
			p.mu.RUnlock()
			logrus.WithFields(logrus.Fields{
				"tool_name":  req.ToolName,
				"request_id": req.ID,
			}).Info("Tool is whitelisted, auto-approving")
			return ask.Result{
				ID:           req.ID,
				Approved:     true,
				UpdatedInput: req.Input,
				Reason:       "Tool is whitelisted (Always Allow)",
			}, nil
		}
		p.mu.RUnlock()
	}

	// Use UUID to get bot (preferred) or fall back to platform lookup
	var bot imbot.Bot
	bot = p.manager.GetBot(botUUID, platform)
	if bot == nil {
		logrus.WithFields(logrus.Fields{
			"id":       req.ID,
			"platform": platform,
			"botUUID":  botUUID,
		}).Warn("Bot not found for request, auto-approving")
		return ask.Result{
			ID:           req.ID,
			Approved:     true,
			UpdatedInput: req.Input,
		}, nil
	}

	// Create response channel
	responseChan := make(chan ask.Result, 1)

	// Count questions for AskUserQuestion requests. The "questions" payload
	// can arrive as []interface{}, []map[string]any, or []any depending on
	// the caller — normalize once.
	totalQuestions := 0
	if req.ToolName == "AskUserQuestion" {
		totalQuestions = len(ask.NormalizeQuestions(req.Input["questions"]))
	}

	p.mu.Lock()
	p.pendingRequests[req.ID] = &pendingIMRequest{
		request:        req,
		chatID:         chatID,
		platform:       platform,
		createdAt:      time.Now(),
		partialAnswers: make(map[string]string),
		totalQuestions: totalQuestions,
	}
	p.responseChannels[req.ID] = responseChan
	timeout := p.defaultTimeout
	if req.Timeout > 0 {
		timeout = req.Timeout
	}
	p.mu.Unlock()

	// Check if platform supports inline keyboards
	supportsKeyboard := imbot.GetPlatformCapabilities(string(platform)).SupportsInteraction()

	// Build prompt using tool handler (pass platform for text fallback)
	promptText := p.buildPromptText(req, supportsKeyboard)
	keyboard := p.buildKeyboard(req)

	// For platforms without keyboard support, append text-based instructions
	if !supportsKeyboard {
		if req.ToolName == "AskUserQuestion" {
			// Append option selection instructions
			promptText += "\n\n" + p.buildTextSelectionInstructions(req)
		} else {
			// Append permission response instructions
			promptText += "\n\n" + p.buildTextPermissionInstructions()
		}
	}

	// Send the prompt message (only include keyboard markup if platform supports it)
	opts := &imbot.SendMessageOptions{
		Text:      promptText,
		ParseMode: imbot.ParseModeMarkdown,
	}
	if supportsKeyboard {
		opts.Metadata = map[string]interface{}{
			"replyMarkup": imbot.BuildTelegramActionKeyboard(keyboard),
		}
	}
	msg, err := bot.SendMessage(context.Background(), chatID, opts)
	if err != nil {
		p.cleanup(req.ID)
		logrus.WithError(err).WithField("id", req.ID).Error("Failed to send prompt")
		return ask.Result{ID: req.ID, Approved: false}, fmt.Errorf("failed to send prompt: %w", err)
	}

	// Store message ID for potential editing
	p.mu.Lock()
	if pending, exists := p.pendingRequests[req.ID]; exists {
		pending.messageID = msg.MessageID
	}
	p.mu.Unlock()

	logrus.WithFields(logrus.Fields{
		"id":        req.ID,
		"chat_id":   chatID,
		"tool_name": req.ToolName,
	}).Info("Prompt sent, waiting for response")

	// Wait for response or timeout
	select {
	case result := <-responseChan:
		p.cleanup(req.ID)
		// Edit message to show result
		p.editPromptToResult(bot, chatID, msg.MessageID, req, result.Approved)
		return result, nil

	case <-time.After(timeout):
		p.cleanup(req.ID)
		p.editPromptToTimeout(bot, chatID, msg.MessageID, req)

		// For AskUserQuestion: auto-select first option (recommended strategy)
		// For permission/approval requests: deny on timeout
		if req.ToolName == "AskUserQuestion" {
			result := p.buildTimeoutQuestionResult(req)
			return result, nil
		}
		return ask.Result{
			ID:       req.ID,
			Approved: false,
			Reason:   "request timed out",
		}, fmt.Errorf("request timed out")

	case <-ctx.Done():
		p.cleanup(req.ID)
		return ask.Result{ID: req.ID, Approved: false}, ctx.Err()
	}
}

// buildTimeoutQuestionResult builds a result that auto-selects the first (recommended) option
// for AskUserQuestion when it times out.
func (p *IMPrompter) buildTimeoutQuestionResult(req ask.Request) ask.Result {
	questions := ask.NormalizeQuestions(req.Input["questions"])
	if len(questions) == 0 {
		return ask.Result{
			ID:       req.ID,
			Approved: false,
			Reason:   "request timed out",
		}
	}

	// For each question, select its first option as the recommended default
	answers := make(map[string]interface{})
	for _, question := range questions {
		questionText, ok := question["question"].(string)
		if !ok {
			continue
		}
		options := ask.NormalizeOptions(question["options"])
		if len(options) == 0 {
			continue
		}
		if label, ok := options[0]["label"].(string); ok {
			answers[questionText] = label
		}
	}

	updatedInput := make(map[string]interface{})
	for k, v := range req.Input {
		updatedInput[k] = v
	}
	updatedInput["answers"] = answers

	return ask.Result{
		ID:           req.ID,
		Approved:     true,
		UpdatedInput: updatedInput,
		Reason:       "timed out - auto-selected recommended option",
	}
}

// SubmitResult submits a result for a pending request
func (p *IMPrompter) SubmitResult(requestID string, result ask.Result) error {
	p.mu.Lock()
	responseChan, exists := p.responseChannels[requestID]
	if !exists {
		p.mu.Unlock()
		return fmt.Errorf("request not found or expired: %s", requestID)
	}

	// Get pending request for tool name (needed for whitelist)
	pending, hasPending := p.pendingRequests[requestID]

	// If remember is true and approved, add tool to whitelist
	if result.Remember && result.Approved && hasPending && pending.request.ToolName != "" {
		p.whitelist[pending.request.ToolName] = true
		logrus.WithFields(logrus.Fields{
			"tool_name":  pending.request.ToolName,
			"request_id": requestID,
		}).Info("Tool added to whitelist (Always Allow)")
	}

	// Keep lock held while sending to prevent race with cleanup
	select {
	case responseChan <- result:
		p.mu.Unlock()
		logrus.WithFields(logrus.Fields{
			"request_id": requestID,
			"approved":   result.Approved,
			"remember":   result.Remember,
		}).Info("Result submitted")
		return nil
	default:
		p.mu.Unlock()
		return fmt.Errorf("response channel full for request: %s", requestID)
	}
}

// SubmitUserResponse submits a user response for a pending request
// This parses the response using the appropriate tool handler
func (p *IMPrompter) SubmitUserResponse(requestID string, response ask.Response) error {
	// Get the pending request
	p.mu.RLock()
	pending, exists := p.pendingRequests[requestID]
	p.mu.RUnlock()

	if !exists {
		return fmt.Errorf("request not found or expired: %s", requestID)
	}

	// Find the appropriate handler and parse the response
	parser := p.registry.FindResponseParser(pending.request.ToolName, pending.request.Input)
	if parser == nil {
		return fmt.Errorf("no handler found for tool: %s", pending.request.ToolName)
	}

	result, err := parser.ParseResponse(pending.request, response)
	if err != nil {
		return fmt.Errorf("failed to parse response: %w", err)
	}

	return p.SubmitResult(requestID, result)
}

// SubmitPartialAnswer records the answer for one question of an AskUserQuestion request.
// Returns (true, nil) when all questions are answered and the result has been submitted.
// Returns (false, nil) when more questions remain.
func (p *IMPrompter) SubmitPartialAnswer(requestID, questionText, label string) (bool, error) {
	p.mu.Lock()
	pending, exists := p.pendingRequests[requestID]
	if !exists {
		p.mu.Unlock()
		return false, fmt.Errorf("request not found or expired: %s", requestID)
	}

	pending.partialAnswers[questionText] = label
	answered := len(pending.partialAnswers)
	total := pending.totalQuestions

	if answered < total {
		p.mu.Unlock()
		return false, nil
	}

	// All questions answered — build final result
	answers := make(map[string]interface{}, len(pending.partialAnswers))
	for k, v := range pending.partialAnswers {
		answers[k] = v
	}
	updatedInput := make(map[string]interface{})
	for k, v := range pending.request.Input {
		updatedInput[k] = v
	}
	updatedInput["answers"] = answers

	result := ask.Result{
		ID:           requestID,
		Approved:     true,
		UpdatedInput: updatedInput,
		Reason:       "User selected options",
	}
	p.mu.Unlock()

	return true, p.SubmitResult(requestID, result)
}

// GetPendingRequest returns a pending request by ID
func (p *IMPrompter) GetPendingRequest(requestID string) (*ask.Request, bool) {
	p.mu.RLock()
	defer p.mu.RUnlock()

	if pending, exists := p.pendingRequests[requestID]; exists {
		return &pending.request, true
	}
	return nil, false
}

// GetPendingRequestsForChat returns all pending requests for a specific chat
func (p *IMPrompter) GetPendingRequestsForChat(chatID string) []ask.Request {
	p.mu.RLock()
	defer p.mu.RUnlock()

	var requests []ask.Request
	for _, pending := range p.pendingRequests {
		if pending.chatID == chatID {
			requests = append(requests, pending.request)
		}
	}
	return requests
}

// buildPromptText builds the prompt message text using tool handlers
func (p *IMPrompter) buildPromptText(req ask.Request, supportsKeyboard bool) string {
	// Try to use tool-specific prompt builder
	builder := p.registry.FindPromptBuilder(req.ToolName, req.Input)
	if builder != nil {
		prompt := builder.BuildPrompt(req)
		logrus.WithFields(logrus.Fields{
			"tool_name":  req.ToolName,
			"prompt_len": len(prompt),
		}).Debug("Built prompt using tool-specific builder")
		return prompt
	}

	// Fallback to default prompt
	logrus.WithField("tool_name", req.ToolName).Debug("No specific builder found, using default prompt")
	return fmt.Sprintf("🔐 *Tool Permission Request*\n\nTool: `%s`", req.ToolName)
}

// buildKeyboard builds the inline keyboard for prompts
func (p *IMPrompter) buildKeyboard(req ask.Request) imbot.InlineKeyboardMarkup {
	// Check if this is AskUserQuestion - build option keyboard
	if req.ToolName == "AskUserQuestion" {
		return p.buildAskUserQuestionKeyboard(req)
	}

	// Default keyboard: Approve/Deny/Always
	return p.buildDefaultKeyboard(req.ID)
}

// buildDefaultKeyboard builds the permission keyboard from shared PermissionOptions config
func (p *IMPrompter) buildDefaultKeyboard(requestID string) imbot.InlineKeyboardMarkup {
	kb := imbot.NewKeyboardBuilder()

	for _, opt := range ask.PermissionOptions {
		buttonText := opt.Icon + " " + opt.Label
		callbackData := imbot.FormatCallbackData("perm", opt.Action, requestID)
		kb.AddRow(imbot.CallbackButton(buttonText, callbackData))
	}

	return kb.Build()
}

// buildAskUserQuestionKeyboard builds a keyboard with options for AskUserQuestion
// Supports multiple questions: callback data includes question index and option index.
func (p *IMPrompter) buildAskUserQuestionKeyboard(req ask.Request) imbot.InlineKeyboardMarkup {
	kb := imbot.NewKeyboardBuilder()

	questions := ask.NormalizeQuestions(req.Input["questions"])
	if len(questions) == 0 {
		return p.buildDefaultKeyboard(req.ID)
	}

	// Build buttons for each question and its options
	for qIdx, question := range questions {
		options := ask.NormalizeOptions(question["options"])
		if len(options) == 0 {
			continue
		}
		// Add a label row for the question if there are multiple questions
		if len(questions) > 1 {
			questionText, _ := question["question"].(string)
			kb.AddRow(imbot.CallbackButton(fmt.Sprintf("Q%d: %s", qIdx+1, questionText), imbot.FormatCallbackData("perm", "noop", req.ID)))
		}
		for optIdx, option := range options {
			label, _ := option["label"].(string)
			if label != "" {
				buttonText := fmt.Sprintf("%d. %s", optIdx+1, label)
				callbackData := imbot.FormatCallbackData("perm", "option", req.ID, fmt.Sprintf("%d", qIdx), fmt.Sprintf("%d", optIdx))
				kb.AddRow(imbot.CallbackButton(buttonText, callbackData))
			}
		}
	}

	// Add cancel button
	kb.AddRow(imbot.CallbackButton("❌ Cancel", imbot.FormatCallbackData("perm", "deny", req.ID)))

	return kb.Build()
}

// buildTextSelectionInstructions builds text instructions for platforms without keyboard support
func (p *IMPrompter) buildTextSelectionInstructions(req ask.Request) string {
	var text strings.Builder

	questions := ask.NormalizeQuestions(req.Input["questions"])
	if len(questions) == 0 {
		// Fallback: use permission instructions from shared config
		return ask.FormatPermissionInstructions()
	}

	// For AskUserQuestion - list all questions and options
	for qIdx, question := range questions {
		questionText, _ := question["question"].(string)
		options := ask.NormalizeOptions(question["options"])
		if len(options) == 0 {
			continue
		}
		if len(questions) > 1 {
			text.WriteString(fmt.Sprintf("*Q%d: %s*\n", qIdx+1, questionText))
		} else {
			text.WriteString("*To select an option, reply with the number:*\n\n")
		}
		for i, option := range options {
			label, _ := option["label"].(string)
			desc, hasDesc := option["description"].(string)
			if hasDesc && desc != "" {
				text.WriteString(fmt.Sprintf("• `%d` - %s - %s\n", i+1, label, desc))
			} else {
				text.WriteString(fmt.Sprintf("• `%d` - %s\n", i+1, label))
			}
		}
		text.WriteString("\n")
	}

	if len(questions) > 1 {
		text.WriteString("_Reply with answers in order, e.g. `1 2 1` for Q1=opt1, Q2=opt2, Q3=opt1_")
	} else {
		text.WriteString("_Just type the number to reply_")
	}

	return text.String()
}

// buildTextPermissionInstructions builds text instructions for permission prompts
// on platforms without keyboard support. Delegates to the shared config in ask package.
func (p *IMPrompter) buildTextPermissionInstructions() string {
	return ask.FormatPermissionInstructions()
}

// editPromptToResult edits the prompt message to show the result
func (p *IMPrompter) editPromptToResult(bot imbot.Bot, chatID, messageID string, req ask.Request, approved bool) {
	resultText := p.buildPromptText(req, true) // supportsKeyboard doesn't matter for result
	if approved {
		resultText += "\n\n✅ *Approved*"
	} else {
		resultText += "\n\n❌ *Denied*"
	}

	// Edit message to remove keyboard and show result
	if tgBot, ok := imbot.AsTelegramBot(bot); ok {
		_ = tgBot.EditMessageWithKeyboard(context.Background(), chatID, messageID, resultText, nil)
	} else {
		// Fallback: send a new message with the result
		_, _ = bot.SendMessage(context.Background(), chatID, &imbot.SendMessageOptions{
			Text:      resultText,
			ParseMode: imbot.ParseModeMarkdown,
		})
	}
}

// editPromptToTimeout edits the prompt message to show timeout
func (p *IMPrompter) editPromptToTimeout(bot imbot.Bot, chatID, messageID string, req ask.Request) {
	resultText := p.buildPromptText(req, true) // supportsKeyboard doesn't matter for timeout
	resultText += "\n\n⏰ *Timed Out*"

	if tgBot, ok := imbot.AsTelegramBot(bot); ok {
		_ = tgBot.EditMessageWithKeyboard(context.Background(), chatID, messageID, resultText, nil)
	}
}

// cleanup removes a pending request and its response channel
func (p *IMPrompter) cleanup(requestID string) {
	p.mu.Lock()
	defer p.mu.Unlock()

	delete(p.pendingRequests, requestID)
	delete(p.responseChannels, requestID)
}

// ParseTextResponse parses user text input as a permission response
// Returns: (approved, remember, isValid)
func ParseTextResponse(text string) (approved bool, remember bool, isValid bool) {
	return ask.ParseTextResponse(text)
}

// GetWhitelist returns the list of whitelisted tools
func (p *IMPrompter) GetWhitelist() []string {
	p.mu.RLock()
	defer p.mu.RUnlock()

	tools := make([]string, 0, len(p.whitelist))
	for tool := range p.whitelist {
		tools = append(tools, tool)
	}
	return tools
}

// AddToWhitelist adds a tool to the whitelist
func (p *IMPrompter) AddToWhitelist(toolName string) {
	p.mu.Lock()
	defer p.mu.Unlock()

	p.whitelist[toolName] = true
	logrus.WithField("tool_name", toolName).Info("Tool added to whitelist")
}

// RemoveFromWhitelist removes a tool from the whitelist
func (p *IMPrompter) RemoveFromWhitelist(toolName string) {
	p.mu.Lock()
	defer p.mu.Unlock()

	delete(p.whitelist, toolName)
	logrus.WithField("tool_name", toolName).Info("Tool removed from whitelist")
}

// ClearWhitelist clears all tools from the whitelist
func (p *IMPrompter) ClearWhitelist() {
	p.mu.Lock()
	defer p.mu.Unlock()

	p.whitelist = make(map[string]bool)
	logrus.Info("Whitelist cleared")
}

// IsWhitelisted checks if a tool is in the whitelist
func (p *IMPrompter) IsWhitelisted(toolName string) bool {
	p.mu.RLock()
	defer p.mu.RUnlock()

	return p.whitelist[toolName]
}

// normalizeQuestionList re-exports ask.NormalizeQuestions for the local tests.
// The shared implementation lives in agentboot/ask/normalize.go so the same
// coercion logic is used by every prompter (IM, stdin, tool_handlers).
func normalizeQuestionList(v any) []map[string]any { return ask.NormalizeQuestions(v) }

// Legacy compatibility methods

// OnApproval implements agentboot.ApprovalHandler.
// It handles permission confirmation requests via IM.
func (p *IMPrompter) OnApproval(ctx context.Context, req agentboot.PermissionRequest) (agentboot.PermissionResult, error) {
	askReq := ask.FromPermissionRequest(req)
	result, err := p.Prompt(ctx, *askReq)
	if err != nil {
		return agentboot.PermissionResult{}, err
	}
	return result.ToPermissionResult(), nil
}

// OnAsk implements agentboot.AskHandler.
// It handles user questions/selections via IM.
func (p *IMPrompter) OnAsk(ctx context.Context, req agentboot.AskRequest) (agentboot.AskResult, error) {
	// Convert AskRequest to ask.Request
	askReq := ask.Request{
		ID:        req.ID,
		Type:      ask.Type(req.Type),
		ChatID:    req.ChatID,
		Platform:  req.Platform,
		BotUUID:   req.BotUUID,
		SessionID: req.SessionID,
		AgentType: req.AgentType,
		ToolName:  req.ToolName,
		Input:     req.Input,
		Message:   req.Message,
		Reason:    req.Reason,
		Metadata:  req.Metadata,
	}

	result, err := p.Prompt(ctx, askReq)
	if err != nil {
		return agentboot.AskResult{}, err
	}

	// Convert back to AskResult
	return agentboot.AskResult{
		ID:           result.ID,
		Approved:     result.Approved,
		Response:     result.Response,
		Selection:    result.Selection,
		Remember:     result.Remember,
		Reason:       result.Reason,
		UpdatedInput: result.UpdatedInput,
	}, nil
}

// SubmitDecision submits a user's decision for a pending request
func (p *IMPrompter) SubmitDecision(requestID string, approved bool, remember bool, reason string) error {
	result := ask.Result{
		ID:       requestID,
		Approved: approved,
		Remember: remember,
		Reason:   reason,
	}
	return p.SubmitResult(requestID, result)
}

// PromptPermission implements the legacy agentboot.UserPrompter interface
func (p *IMPrompter) PromptPermission(ctx context.Context, req agentboot.PermissionRequest) (agentboot.PermissionResult, error) {
	return p.OnApproval(ctx, req)
}
