package modes

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

	"github.com/patriceckhart/zot/packages/core"
	"github.com/patriceckhart/zot/packages/tui"
)

// confirmRequest is one pending confirmation. The agent goroutine
// writes it onto the interactive's queue, the TUI renders the
// dialog, and the user's answer goes back through `resp`.
type confirmRequest struct {
	toolName string
	preview  string
	resp     chan core.ConfirmDecision
}

// confirmDialog is the inline prompt shown before every tool call
// when --no-yolo is on. It presents four options and routes the
// answer back through the pending request's response channel.
type confirmDialog struct {
	mu      sync.Mutex
	pending []*confirmRequest
	cursor  int
	// activeSince timestamps when the current request started
	// showing. Used only for debug/future UX (e.g. flashing the
	// dialog border after N seconds of no answer).
	activeSince time.Time
}

func newConfirmDialog() *confirmDialog { return &confirmDialog{} }

// Enqueue adds a pending request. Multiple requests can queue up
// if the agent produces tool calls in parallel (via extensions);
// the TUI shows them one at a time, in order.
func (d *confirmDialog) Enqueue(req *confirmRequest) {
	d.mu.Lock()
	defer d.mu.Unlock()
	d.pending = append(d.pending, req)
	if len(d.pending) == 1 {
		d.cursor = 0
		d.activeSince = time.Now()
	}
}

// Active reports whether the dialog is consuming input (there's at
// least one pending request).
func (d *confirmDialog) Active() bool {
	if d == nil {
		return false
	}
	d.mu.Lock()
	defer d.mu.Unlock()
	return len(d.pending) > 0
}

// CancelAll refuses every pending request (used when the user
// cancels the active turn or closes the session while the dialog
// is open). Safe to call when idle.
func (d *confirmDialog) CancelAll(reason string) {
	d.mu.Lock()
	pending := d.pending
	d.pending = nil
	d.mu.Unlock()
	for _, req := range pending {
		select {
		case req.resp <- core.ConfirmDecision{Allow: false, Reason: reason}:
		default:
		}
	}
}

// AllowAllPending approves every pending request and drains the
// queue. Used by /yolo so any confirmation dialog already on screen
// resolves immediately as "yes".
func (d *confirmDialog) AllowAllPending() {
	d.mu.Lock()
	pending := d.pending
	d.pending = nil
	d.mu.Unlock()
	for _, req := range pending {
		select {
		case req.resp <- core.ConfirmDecision{Allow: true, RememberAll: true}:
		default:
		}
	}
}

// confirmOptions lists the four responses in vertical order. Keyed
// by index so HandleKey can send the right decision.
var confirmOptions = []struct {
	label    string
	decision core.ConfirmDecision
}{
	{
		label:    "yes (run this call)",
		decision: core.ConfirmDecision{Allow: true},
	},
	{
		label:    "yes, always this tool (skip prompts for this tool for the rest of the session)",
		decision: core.ConfirmDecision{Allow: true, RememberTool: true},
	},
	{
		label:    "yes, always (skip all prompts for the rest of the session)",
		decision: core.ConfirmDecision{Allow: true, RememberAll: true},
	},
	{
		label:    "no (refuse and let the model try something else)",
		decision: core.ConfirmDecision{Allow: false, Reason: "user declined"},
	},
}

// HandleKey advances the selection or resolves the dialog. Returns
// true when a decision was sent back to the agent (the dialog may
// remain Active if there are more queued requests behind it).
func (d *confirmDialog) HandleKey(k tui.Key) bool {
	d.mu.Lock()
	if len(d.pending) == 0 {
		d.mu.Unlock()
		return false
	}
	req := d.pending[0]

	switch k.Kind {
	case tui.KeyUp:
		if d.cursor > 0 {
			d.cursor--
		}
		d.mu.Unlock()
		return false
	case tui.KeyDown:
		if d.cursor < len(confirmOptions)-1 {
			d.cursor++
		}
		d.mu.Unlock()
		return false
	case tui.KeyEsc, tui.KeyCtrlC:
		// Treat esc/ctrl+c as "no, refuse this tool call". The
		// active turn can separately be cancelled by the outer esc
		// handler, but for the dialog itself, we must always answer
		// so the agent goroutine unblocks.
		d.pending = d.pending[1:]
		if len(d.pending) > 0 {
			d.cursor = 0
			d.activeSince = time.Now()
		}
		d.mu.Unlock()
		req.resp <- core.ConfirmDecision{Allow: false, Reason: "user cancelled"}
		return true
	case tui.KeyEnter:
		selected := confirmOptions[d.cursor].decision
		d.pending = d.pending[1:]
		if len(d.pending) > 0 {
			d.cursor = 0
			d.activeSince = time.Now()
		}
		d.mu.Unlock()
		req.resp <- selected
		return true
	}

	// Numeric shortcuts 1..4
	if k.Kind == tui.KeyRune && k.Rune >= '1' && k.Rune <= '4' {
		idx := int(k.Rune - '1')
		if idx >= 0 && idx < len(confirmOptions) {
			selected := confirmOptions[idx].decision
			d.pending = d.pending[1:]
			if len(d.pending) > 0 {
				d.cursor = 0
				d.activeSince = time.Now()
			}
			d.mu.Unlock()
			req.resp <- selected
			return true
		}
	}

	d.mu.Unlock()
	return false
}

// Render draws the dialog for the head-of-queue request.
func (d *confirmDialog) Render(th tui.Theme, width int) []string {
	d.mu.Lock()
	defer d.mu.Unlock()
	if len(d.pending) == 0 {
		return nil
	}
	req := d.pending[0]
	cursor := d.cursor
	queued := len(d.pending) - 1

	var lines []string
	title := "confirm tool call"
	if queued > 0 {
		title = fmt.Sprintf("confirm tool call  (%d more queued)", queued)
	}
	lines = append(lines, frameHeader(th, title, width))

	toolLine := "  " + th.FG256(th.Tool, req.toolName)
	if req.preview != "" {
		toolLine += "  " + th.FG256(th.Muted, req.preview)
	}
	lines = append(lines, toolLine)
	lines = append(lines, "")
	lines = append(lines, th.FG256(th.Muted, "choose (\u2191/\u2193 or 1-4, enter to pick, esc to refuse):"))

	for i, opt := range confirmOptions {
		label := opt.label
		prefix := fmt.Sprintf("  %d. ", i+1)
		plain := prefix + label
		// Truncate the tail if the line would exceed width; keeps
		// the option numbers always visible.
		if visibleLen(plain) > width-2 {
			if width <= 5 {
				plain = "..."[:max(0, width-2)]
			} else {
				plain = plain[:width-5] + "..."
			}
		}
		if i == cursor {
			lines = append(lines, th.PadHighlight(plain, width))
		} else {
			lines = append(lines, th.FG256(th.Muted, plain))
		}
	}
	lines = append(lines, frameRule(th, width))
	return lines
}

// visibleLen is a cheap char-count; ANSI codes aren't in the raw
// text at this point so len() is fine.
func visibleLen(s string) int { return strings.Count(s, "") - 1 }
