package app

import (
	"sort"
	"strings"

	"github.com/charmbracelet/lipgloss"
)

// center composites a modal over a scrim-filled screen. Immediate-mode: the modal
// is recomputed from state every frame, so there is no separate show/restore path
// to desync (the "diff won't come back" bug class can't occur here).
func (m Model) center(modal string) string {
	return lipgloss.Place(m.width, m.height, lipgloss.Center, lipgloss.Center, modal,
		lipgloss.WithWhitespaceBackground(m.theme.P.BgDeep),
		lipgloss.WithWhitespaceForeground(m.theme.P.BgDeep))
}

func (m Model) modalWidth() int {
	w := m.width * 70 / 100
	if w > 92 {
		w = 92
	}
	if w < 24 {
		w = 24
	}
	return w
}

func (m Model) renderOverlay(base string) string {
	switch m.overlay {
	case OverlayPalette:
		return m.center(m.paletteModal())
	case OverlayEventInspector:
		return m.center(m.eventInspectorModal())
	case OverlayDiff:
		return m.center(m.diffModal())
	case OverlayToolPalette:
		return m.center(m.toolPaletteModal())
	case OverlayModels:
		return m.center(m.modelsModal())
	}
	return base
}

func (m Model) titleLine(text string) string {
	t := m.theme
	return lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Bold(true).Render(text)
}

func mbg(t Theme, s string, fg lipgloss.Color) string {
	return lipgloss.NewStyle().Foreground(fg).Background(t.P.BgPanel).Render(s)
}

// approvalBandHeight sizes the docked approval band to its diff, capped so it
// never crowds out the session above (longer diffs spill to the ^x fullscreen
// view). Layout: 2 borders + header + blank + diff rows + blank + action row.
func (m Model) approvalBandHeight() int {
	rows, rat := 0, 0
	if s := m.session(m.selectedID); s != nil && s.Pending != nil {
		rows = previewRowCount(s.Pending.Preview)
		if n := len(s.Pending.Rationale); n > 0 {
			rat = n + 1 // rationale lines + trailing blank
		}
	}
	steer := 0
	if m.steering || m.steerBuffer != "" {
		steer = 1
	}
	h := 2 + 1 + 1 + rat + rows + 1 + steer + 1
	if maxH := m.height * 55 / 100; h > maxH {
		h = maxH
	}
	if h < 10 {
		h = 10
	}
	if hardMax := m.height - statusH - footerH - 3; hardMax > 4 && h > hardMax {
		h = hardMax
	}
	return h
}

// renderApprovalBand draws the approval gate as a full-width band (in place of
// the input bar): tool/tier/risk header, a two-column old|new diff, and the
// action row. It never floats over the rest of the UI.
func (m Model) renderApprovalBand(h int) string {
	t := m.theme
	s := m.session(m.selectedID)
	if s == nil || s.Pending == nil {
		return m.renderInput()
	}
	a := s.Pending
	textW := m.width - 4 // box borders (2) + side padding (2)

	riskColor := t.P.Warn
	switch strings.ToUpper(a.Risk) {
	case "HIGH", "CRITICAL":
		riskColor = t.P.Bad
	case "LOW":
		riskColor = t.P.OK
	}

	leftHdr := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("→ ") +
		t.span(a.ToolName, t.P.FgStrong)
	if tgt := diffTarget(a.Preview); tgt != "" {
		leftHdr += t.span(" "+tgt, t.P.Dim)
	}
	rightHdr := t.span("tier ", t.P.Faint) + t.span(a.Tier, t.P.Accent2) +
		t.span("  ·  risk ", t.P.Faint) +
		lipgloss.NewStyle().Foreground(riskColor).Background(t.P.Bg).Bold(true).Render(strings.ToUpper(a.Risk))

	innerH := h - 2
	ratBlock := 0
	if len(a.Rationale) > 0 {
		ratBlock = len(a.Rationale) + 1 // rationale lines + trailing blank
	}
	steerBlock := 0
	if m.steering || m.steerBuffer != "" {
		steerBlock = 1
	}
	diffH := innerH - 4 - ratBlock - steerBlock // header, blank, blank, action row, + rationale/steer
	if diffH < 1 {
		diffH = 1
	}
	body := make([]string, 0, innerH)
	body = append(body, m.justify(leftHdr, rightHdr, textW, t.P.Bg), "")
	for _, r := range a.Rationale {
		marker := lipgloss.NewStyle().Foreground(t.P.Warn).Background(t.P.Bg).Render("▲ ")
		body = append(body, marker+t.span(truncate(r, textW-2), t.P.Dim))
	}
	if len(a.Rationale) > 0 {
		body = append(body, "")
	}
	var content []string
	if isUnifiedDiff(a.Preview) {
		content = m.renderSplitDiff(parseUnifiedDiff(a.Preview), textW, diffH, 0)
	} else {
		content = m.renderPreviewLines(a.Preview, textW, diffH, 0)
	}
	diffStart := len(body)
	body = append(body, content...)
	for len(body) < diffStart+diffH {
		body = append(body, "")
	}
	body = append(body, "")
	if steerBlock == 1 {
		body = append(body, m.steerLine())
	}
	body = append(body, m.approvalActions())
	return t.box("permission required", body, m.width, h, true)
}

// steerLine renders the steering note inside the band: editable (with a caret)
// while the operator is typing it, or as a persistent reminder once set — so it
// stays visible after editing and it's clear it rides along with approve/reject.
func (m Model) steerLine() string {
	t := m.theme
	label := t.span("steer  ", t.P.Faint)
	caret := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▏")
	if m.steering {
		if m.steerBuffer == "" {
			return label + caret + t.span(" type a note…", t.P.Faint)
		}
		return label + t.span(m.steerBuffer, t.P.FgStrong) + caret
	}
	return label + t.span(m.steerBuffer, t.P.Accent2) +
		t.span("   (sent with your decision · s to edit)", t.P.Faint)
}

// approvalActions renders the action row of the approval band. While the operator
// is composing a steer note the keys are different (typing is captured), so the
// hints switch to the editing controls.
func (m Model) approvalActions() string {
	t := m.theme
	hint := func(k, l string) string {
		return lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render(k) +
			t.span(" "+l, t.P.Faint)
	}
	if m.steering {
		return strings.Join([]string{
			hint("enter", "approve + send note"), hint("esc", "keep note, decide below"),
		}, t.span("   ", t.P.Faint))
	}
	parts := []string{hint("a", "approve"), hint("A", "auto"), hint("s", "steer"), hint("r", "reject")}
	if s := m.session(m.selectedID); s != nil && s.Pending != nil && isUnifiedDiff(s.Pending.Preview) {
		parts = append(parts, hint("^x", "fullscreen"))
	}
	parts = append(parts, hint("esc", "later"))
	return strings.Join(parts, t.span("   ", t.P.Faint))
}

// diffBodyHeight is the number of diff lines visible in the diff modal body.
func (m Model) diffBodyHeight() int {
	h := m.height * 70 / 100
	if h < 8 {
		h = 8
	}
	return h - 6
}

// diffMaxScroll is the largest scroll offset that still fills the body with
// diff rows, keeping the last page anchored to the bottom of the modal.
func (m Model) diffMaxScroll() int {
	max := previewRowCount(m.currentDiff()) - m.diffBodyHeight()
	if max < 0 {
		max = 0
	}
	return max
}

func (m Model) diffModal() string {
	t := m.theme
	w := m.modalWidth()
	bodyH := m.diffBodyHeight()
	diff := m.currentDiff()
	isDiff := isUnifiedDiff(diff)

	off := m.diffScrollOffset
	if off > m.diffMaxScroll() {
		off = m.diffMaxScroll()
	}
	if off < 0 {
		off = 0
	}

	var b strings.Builder
	title := "preview"
	if isDiff {
		title = "diff"
	}
	b.WriteString(m.titleLine(title))
	if tgt := diffTarget(diff); tgt != "" {
		b.WriteString(mbg(t, "  "+tgt, t.P.Dim))
	}
	b.WriteString(mbg(t, "   ("+itoa(previewRowCount(diff))+" rows)", t.P.Faint) + "\n\n")
	var lines []string
	if isDiff {
		lines = m.renderSplitDiff(parseUnifiedDiff(diff), w-6, bodyH, off)
	} else {
		lines = m.renderPreviewLines(diff, w-6, bodyH, off)
	}
	for _, ln := range lines {
		b.WriteString(ln + "\n")
	}
	b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "scroll"}, {"^x/esc", "close"}}))

	modal := t.Overlay.Width(w).Render(b.String())
	return m.center(modal)
}

func (m Model) eventInspectorModal() string {
	t := m.theme
	w := m.modalWidth()
	evs := m.currentEvents()

	var b strings.Builder
	b.WriteString(m.titleLine("event inspector"))
	if len(evs) > 0 {
		b.WriteString(mbg(t, "   ("+itoa(m.overlayEventIdx+1)+"/"+itoa(len(evs))+")", t.P.Faint))
	}
	b.WriteString("\n\n")
	if len(evs) == 0 {
		b.WriteString(mbg(t, "no events", t.P.Faint))
		return m.center(t.Overlay.Width(w).Render(b.String()))
	}
	// Budget the plain detail so the styled row never needs ANSI-aware truncation
	// (truncating a styled string would cut through escape codes and corrupt it).
	detailBudget := (w - 6) - (2 + 9 + 11 + 1 + 18 + 1)
	if detailBudget < 4 {
		detailBudget = 4
	}
	// Window the (potentially long) list to a visible height, keeping the
	// selected row in view — the modal must not grow past the screen.
	bodyH := m.height*70/100 - 4
	if bodyH < 3 {
		bodyH = 3
	}
	off := 0
	if len(evs) > bodyH {
		off = m.overlayEventIdx - bodyH/2
		if off < 0 {
			off = 0
		}
		if off > len(evs)-bodyH {
			off = len(evs) - bodyH
		}
	}
	end := off + bodyH
	if end > len(evs) {
		end = len(evs)
	}
	for i := off; i < end; i++ {
		e := evs[i]
		cat := inferCategory(e.Type)
		marker := mbg(t, "  ", t.P.BgPanel)
		fg := t.P.Fg
		if i == m.overlayEventIdx {
			marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▸ ")
			fg = t.P.FgStrong
		}
		row := marker + mbg(t, padRaw(e.Time, 8)+" ", t.P.Faint) +
			lipgloss.NewStyle().Foreground(t.categoryColor(cat)).Background(t.P.BgPanel).Render(padRaw("["+cat+"]", 11)) + " " +
			lipgloss.NewStyle().Foreground(fg).Background(t.P.BgPanel).Render(padRaw(e.Type, 18)) +
			mbg(t, " "+truncate(e.Detail, detailBudget), t.P.Dim)
		b.WriteString(row + "\n")
	}
	b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"esc", "close"}}))

	modal := t.Overlay.Width(w).Render(b.String())
	return m.center(modal)
}

func (m Model) paletteModal() string {
	t := m.theme
	w := m.modalWidth()
	cmds := m.filteredPalette()

	var b strings.Builder
	b.WriteString(m.titleLine("command palette") + "\n\n")

	// filter input line
	filter := m.paletteFilter
	caret := mbg(t, " ", t.P.BgPanel)
	if m.caretVisible() {
		caret = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▏")
	}
	prompt := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("› ")
	if filter == "" {
		b.WriteString(prompt + caret + mbg(t, "type to filter…", t.P.Faint) + "\n\n")
	} else {
		b.WriteString(prompt + mbg(t, filter, t.P.FgStrong) + caret + "\n\n")
	}

	if len(cmds) == 0 {
		b.WriteString(mbg(t, "  no matching commands", t.P.Faint) + "\n")
	}
	for i, c := range cmds {
		marker := mbg(t, "  ", t.P.BgPanel)
		titleFg := t.P.Fg
		if i == m.paletteIndex {
			marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▌ ")
			titleFg = t.P.FgStrong
		}
		b.WriteString(marker +
			lipgloss.NewStyle().Foreground(titleFg).Background(t.P.BgPanel).Render(padRaw(c.title, 18)) +
			mbg(t, "  "+c.hint, t.P.Faint) + "\n")
	}
	b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"enter", "run"}, {"esc", "close"}}))
	modal := t.Overlay.Width(w).Render(b.String())
	return m.center(modal)
}

func (m Model) toolPaletteModal() string {
	t := m.theme
	w := m.modalWidth()
	s := m.session(m.selectedID)

	var b strings.Builder
	b.WriteString(m.titleLine("tool palette") + "\n\n")
	if s == nil || len(s.ToolsByStage) == 0 {
		b.WriteString(mbg(t, "no tool manifest for this session", t.P.Faint) + "\n")
	} else {
		stages := make([]string, 0, len(s.ToolsByStage))
		for st := range s.ToolsByStage {
			stages = append(stages, st)
		}
		sort.Strings(stages)
		for _, st := range stages {
			label := mbg(t, st, t.P.Accent2)
			if st == s.CurrentStage {
				label += lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("  ◂ current")
			}
			b.WriteString(label + "\n")
			for _, tool := range s.ToolsByStage[st] {
				tier := lipgloss.NewStyle().Foreground(tierColor(t, tool.Tier)).Background(t.P.BgPanel).Render("T" + itoa(tool.Tier))
				b.WriteString(mbg(t, "    "+padRaw(tool.Name, 22), t.P.Fg) + tier + "\n")
			}
		}
	}
	b.WriteString("\n" + modalHints(t, [][2]string{{"t/esc", "close"}}))
	modal := t.Overlay.Width(w).Render(b.String())
	return m.center(modal)
}

func (m Model) modelsModal() string {
	t := m.theme
	w := m.modalWidth()

	var b strings.Builder
	b.WriteString(m.titleLine("models") + "\n\n")

	if g := m.gaugeText(); g != "" {
		b.WriteString(mbg(t, g, t.P.Faint) + "\n\n")
	}

	if len(m.availableModels) == 0 {
		b.WriteString(mbg(t, "  model management not enabled", t.P.Faint) + "\n")
	}
	for i, id := range m.availableModels {
		marker := mbg(t, "  ", t.P.BgPanel)
		nameFg := t.P.Fg
		if i == m.modelsIndex {
			marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▌ ")
			nameFg = t.P.FgStrong
		}
		line := marker + lipgloss.NewStyle().Foreground(nameFg).Background(t.P.BgPanel).Render(padRaw(id, 28))
		if id == m.currentModel {
			line += lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.BgPanel).Render("  ◂ resident")
		}
		b.WriteString(line + "\n")
	}
	b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"enter", "swap"}, {"c", "clear pin"}, {"esc", "close"}}))
	modal := t.Overlay.Width(w).Render(b.String())
	return m.center(modal)
}

func tierColor(t Theme, tier int) lipgloss.Color {
	switch {
	case tier >= 3:
		return t.P.Bad
	case tier == 2:
		return t.P.Warn
	case tier == 1:
		return t.P.Accent2
	default:
		return t.P.OK
	}
}

func modalHints(t Theme, pairs [][2]string) string {
	parts := make([]string, 0, len(pairs))
	for _, p := range pairs {
		parts = append(parts,
			lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Bold(true).Render(p[0])+
				mbg(t, " "+p[1], t.P.Faint))
	}
	return strings.Join(parts, mbg(t, "   ", t.P.Faint))
}

func truncate(s string, w int) string {
	if len([]rune(s)) <= w {
		return s
	}
	r := []rune(s)
	if w < 1 {
		return ""
	}
	return string(r[:w-1]) + "…"
}
