package app

import (
	"image/color"
	"strconv"
	"strings"

	"charm.land/lipgloss/v2"
)

type diffKind int

const (
	diffBlank diffKind = iota // no line on this side
	diffContext
	diffDel
	diffAdd
)

// diffRow is one aligned row of a split (side-by-side) diff: the old line on the
// left, the new line on the right. Either side may be blank (pure add/remove).
type diffRow struct {
	oldNum, newNum   int // 0 = blank cell
	oldText, newText string
	oldKind, newKind diffKind
}

// parseUnifiedDiff turns a unified diff into aligned old|new rows. File headers
// (---, +++, diff, index) are dropped; @@ markers seed the line numbers.
// Consecutive removals and additions are zipped so an edit lines up; a pure
// create yields blank left cells (and vice-versa for a delete).
func parseUnifiedDiff(diff string) []diffRow {
	var rows []diffRow
	oldN, newN := 1, 1
	var dels, adds []string
	flush := func() {
		n := len(dels)
		if len(adds) > n {
			n = len(adds)
		}
		for i := 0; i < n; i++ {
			r := diffRow{}
			if i < len(dels) {
				r.oldNum, r.oldText, r.oldKind = oldN, dels[i], diffDel
				oldN++
			}
			if i < len(adds) {
				r.newNum, r.newText, r.newKind = newN, adds[i], diffAdd
				newN++
			}
			rows = append(rows, r)
		}
		dels, adds = dels[:0], adds[:0]
	}
	for _, ln := range strings.Split(strings.TrimRight(diff, "\n"), "\n") {
		switch {
		// File headers carry a trailing space ("+++ b/x"); requiring it means a real
		// changed line whose content starts with "++"/"--" isn't mistaken for a header
		// and silently dropped from the rendered diff (and its row count).
		case strings.HasPrefix(ln, "+++ "), strings.HasPrefix(ln, "--- "),
			strings.HasPrefix(ln, "diff "), strings.HasPrefix(ln, "index "):
			continue
		case strings.HasPrefix(ln, "@@"):
			flush()
			oldN, newN = hunkStarts(ln)
		case strings.HasPrefix(ln, "-"):
			dels = append(dels, ln[1:])
		case strings.HasPrefix(ln, "+"):
			adds = append(adds, ln[1:])
		default: // context (leading space) or stray blank line
			flush()
			text := strings.TrimPrefix(ln, " ")
			rows = append(rows, diffRow{
				oldNum: oldN, newNum: newN, oldText: text, newText: text,
				oldKind: diffContext, newKind: diffContext,
			})
			oldN++
			newN++
		}
	}
	flush()
	return rows
}

// isUnifiedDiff reports whether s looks like a unified diff (file/hunk headers)
// rather than arbitrary preview text such as a shell argv JSON. Non-diff previews
// must not go through the two-column renderer (it would show identical columns).
func isUnifiedDiff(s string) bool {
	return strings.Contains(s, "@@ -") || strings.HasPrefix(s, "--- ") || strings.Contains(s, "\n--- ")
}

// previewPlainLines splits non-diff preview text into display lines.
func previewPlainLines(s string) []string {
	return strings.Split(strings.TrimRight(s, "\n"), "\n")
}

// previewDisplayLines is the single source of truth for how a non-diff preview is laid out:
// a scannable summary for structured (JSON) artifacts, raw lines otherwise. Both the renderer
// and previewRowCount go through it so band sizing and scroll bounds match what's drawn.
func previewDisplayLines(s string) []string {
	if lines, ok := artifactDisplayLines(s); ok {
		return lines
	}
	return previewPlainLines(s)
}

// previewRowCount is the number of content rows a preview occupies — diff rows
// for a real diff, plain lines otherwise. Drives band sizing and scroll bounds.
func previewRowCount(s string) int {
	if isUnifiedDiff(s) {
		return len(parseUnifiedDiff(s))
	}
	return len(previewDisplayLines(s))
}

// renderPreviewLines renders non-diff preview text as dim plain lines (single
// column), up to maxRows starting at off, each truncated to width.
func (m Model) renderPreviewLines(content string, width, maxRows, off int) []string {
	lines := previewDisplayLines(content)
	if off < 0 {
		off = 0
	}
	if off > len(lines) {
		off = len(lines)
	}
	end := off + maxRows
	if end > len(lines) {
		end = len(lines)
	}
	out := make([]string, 0, end-off)
	for _, ln := range lines[off:end] {
		out = append(out, m.colorizePreviewLine(ln, width))
	}
	return out
}

// colorizePreviewLine highlights a summarized-artifact line: flush-left section headers (Goal,
// Plan · N) in accent, the "→ produces" target picked out, everything else dim — so the structure
// reads at a glance without a full JSON syntax highlighter. Plain (raw) previews stay uniformly dim.
func (m Model) colorizePreviewLine(ln string, w int) string {
	t := m.theme
	ln = truncate(ln, w)
	if ln == "" {
		return ""
	}
	// Section header: no leading indent (e.g. "Goal", "Plan · 4 stages").
	if ln[0] != ' ' {
		return lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.Bg).Bold(true).Render(ln)
	}
	// Stage head: pick out the "→ produces" target after truncation so widths stay bounded.
	if i := strings.Index(ln, " → "); i >= 0 {
		head := t.span(ln[:i], t.P.FgStrong)
		arrow := t.span(" → ", t.P.Faint)
		prod := lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render(ln[i+len(" → "):])
		return head + arrow + prod
	}
	return t.span(ln, t.P.Dim)
}

// hunkStarts reads the starting old/new line numbers from an @@ -a,b +c,d @@ marker.
func hunkStarts(h string) (int, int) {
	old, newer := 1, 1
	for _, f := range strings.Fields(h) {
		switch {
		case strings.HasPrefix(f, "-"):
			old = atoiComma(f[1:])
		case strings.HasPrefix(f, "+"):
			newer = atoiComma(f[1:])
		}
	}
	return old, newer
}

func atoiComma(s string) int {
	if i := strings.IndexByte(s, ','); i >= 0 {
		s = s[:i]
	}
	n, _ := strconv.Atoi(s)
	return n
}

// diffTarget returns the path the diff writes to (the +++ side), stripped of the
// b/ prefix. Empty if the diff carries no header.
func diffTarget(diff string) string {
	for _, ln := range strings.Split(diff, "\n") {
		if strings.HasPrefix(ln, "+++ ") {
			p := strings.Fields(strings.TrimPrefix(ln, "+++ "))[0]
			p = strings.TrimPrefix(p, "b/")
			if p != "/dev/null" {
				return p
			}
		}
	}
	return ""
}

const diffGutter = 4 // line-number column width

// renderSplitDiff renders aligned rows as a two-column old|new view exactly
// `width` cells wide, returning up to maxRows lines starting at offset off.
func (m Model) renderSplitDiff(rows []diffRow, width, maxRows, off int) []string {
	colW := (width - 1) / 2
	if colW < diffGutter+4 {
		colW = diffGutter + 4
	}
	if off < 0 {
		off = 0
	}
	if off > len(rows) {
		off = len(rows)
	}
	end := off + maxRows
	if end > len(rows) {
		end = len(rows)
	}
	sep := lipgloss.NewStyle().Foreground(m.theme.P.Border).Background(m.theme.P.Bg).Render("│")
	out := make([]string, 0, end-off)
	for _, r := range rows[off:end] {
		out = append(out,
			m.diffCell(r.oldNum, r.oldText, r.oldKind, colW)+sep+
				m.diffCell(r.newNum, r.newText, r.newKind, colW))
	}
	return out
}

// diffCell renders one side of a diff row (gutter number, +/- sign, text) padded
// to width w with the kind's background tint.
func (m Model) diffCell(num int, text string, kind diffKind, w int) string {
	t := m.theme
	if kind == diffBlank {
		return lipgloss.NewStyle().Background(t.P.Bg).Render(strings.Repeat(" ", w))
	}
	var bg, textFg, signFg color.Color
	sign := " "
	switch kind {
	case diffAdd:
		bg, textFg, signFg, sign = t.P.DiffAdd, t.P.FgStrong, t.P.OK, "+"
	case diffDel:
		bg, textFg, signFg, sign = t.P.DiffDel, t.P.FgStrong, t.P.Bad, "-"
	default: // context
		bg, textFg, signFg = t.P.Bg, t.P.Dim, t.P.Faint
	}
	numStr := ""
	if num > 0 {
		numStr = itoa(num)
	}
	numCell := lipgloss.NewStyle().Foreground(t.P.Faint).Background(bg).Render(padLeft(numStr, diffGutter) + " ")
	signCell := lipgloss.NewStyle().Foreground(signFg).Background(bg).Bold(true).Render(sign + " ")
	textW := w - diffGutter - 3
	if textW < 1 {
		textW = 1
	}
	body := lipgloss.NewStyle().Foreground(textFg).Background(bg).Render(truncate(text, textW))
	return padTo(numCell+signCell+body, w, bg)
}

// padLeft right-aligns s in a field of width w (space-padded on the left).
func padLeft(s string, w int) string {
	if len(s) >= w {
		return s
	}
	return strings.Repeat(" ", w-len(s)) + s
}
