package ui

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/key"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/mattn/go-runewidth"

	"github.com/unkn0wn-root/resterm/internal/bodyfmt"
	"github.com/unkn0wn-root/resterm/internal/httpclient"
	"github.com/unkn0wn-root/resterm/internal/restfile"
	"github.com/unkn0wn-root/resterm/internal/scripts"
	"github.com/unkn0wn-root/resterm/internal/ui/textarea"
	"github.com/unkn0wn-root/resterm/internal/wrap"
)

func (m *Model) filterEditorMessage(msg tea.Msg) tea.Msg {
	if km, ok := msg.(tea.KeyMsg); ok {
		if m.editorInsertMode {
			if km.Type == tea.KeyTab {
				km.Type = tea.KeyRunes
				km.Runes = []rune{'\t'}
				return km
			}
			return msg
		}

		if km.Type == tea.KeyRunes && len(km.Runes) > 0 {
			km.Runes = nil
			return km
		}
		switch km.String() {
		case "enter", "ctrl+m", "ctrl+j", "backspace", "ctrl+h", "delete":
			km.Type = tea.KeyRunes
			km.Runes = nil
			return km
		}
	}
	return msg
}

func stripANSIEscape(s string) string {
	return bodyfmt.StripANSI(s)
}

var ansiSequenceRegex = bodyfmt.ANSISequenceRegex

func formatTestSummary(results []scripts.TestResult, scriptErr error) string {
	return defaultResponseRenderer().formatTestSummary(results, scriptErr)
}

func (r responseRenderer) formatTestSummary(
	results []scripts.TestResult,
	scriptErr error,
) string {
	if len(results) == 0 && scriptErr == nil {
		return ""
	}

	builder := strings.Builder{}
	builder.WriteString(r.stats.Heading.Render("Tests:") + "\n")
	if scriptErr != nil {
		errorLabel := r.stats.Warn.Render("[ERROR]")
		builder.WriteString(
			"  " + errorLabel + " " + r.stats.Message.Render(scriptErr.Error()) + "\n",
		)
	}
	for _, result := range results {
		statusStyle := r.stats.Success
		statusLabel := "[PASS]"
		if !result.Passed {
			statusStyle = r.stats.Warn
			statusLabel = "[FAIL]"
		}
		line := strings.Builder{}
		line.WriteString("  ")
		line.WriteString(statusStyle.Render(statusLabel))
		if strings.TrimSpace(result.Name) != "" {
			line.WriteString(" ")
			line.WriteString(r.stats.Value.Render(result.Name))
		}
		if strings.TrimSpace(result.Message) != "" {
			line.WriteString(" – ")
			line.WriteString(r.stats.Message.Render(result.Message))
		}
		if result.Elapsed > 0 {
			dur := result.Elapsed.Truncate(time.Millisecond)
			line.WriteString(" ")
			line.WriteString(r.stats.Duration.Render(fmt.Sprintf("(%s)", dur)))
		}
		builder.WriteString(line.String() + "\n")
	}
	return strings.TrimRight(builder.String(), "\n")
}

func (r responseRenderer) buildRespSum(
	resp *httpclient.Response,
	tests []scripts.TestResult,
	scriptErr error,
) string {
	return r.buildRespSummary(resp, tests, scriptErr, false)
}

func (r responseRenderer) buildRespSumPretty(
	resp *httpclient.Response,
	tests []scripts.TestResult,
	scriptErr error,
) string {
	return r.buildRespSummary(resp, tests, scriptErr, true)
}

func (r responseRenderer) buildRespSummary(
	resp *httpclient.Response,
	tests []scripts.TestResult,
	scriptErr error,
	prettyLength bool,
) string {
	if resp == nil {
		return ""
	}

	var lines []string
	statusLine := r.renderStatusLine(resp.Status, resp.StatusCode)
	if statusLine != "" {
		lines = append(lines, statusLine)
	}

	lengthLine := r.renderContentLengthLine(resp)
	if prettyLength {
		lengthLine = r.renderContentLengthLinePretty(resp)
	}
	if lengthLine != "" {
		lines = append(lines, lengthLine)
	}

	if trimmedURL := strings.TrimSpace(resp.EffectiveURL); trimmedURL != "" {
		lines = append(lines, renderLabelValue("URL", trimmedURL, r.stats.Label, r.stats.Value))
	}

	if resp.Headers != nil {
		if streamType := strings.TrimSpace(resp.Headers.Get(streamHeaderType)); streamType != "" {
			lines = append(
				lines,
				renderLabelValue("Stream", streamType, r.stats.Label, r.stats.Value),
			)
		}

		if summary := strings.TrimSpace(resp.Headers.Get(streamHeaderSummary)); summary != "" {
			lines = append(
				lines,
				renderLabelValue("Stream summary", summary, r.stats.Label, r.stats.Message),
			)
		}
	}

	if resp.Duration > 0 {
		dur := resp.Duration.Round(time.Millisecond)
		if dur <= 0 {
			dur = resp.Duration
		}
		lines = append(
			lines,
			renderLabelValue("Duration", dur.String(), r.stats.Label, r.stats.Duration),
		)
	}

	summary := strings.Join(lines, "\n")
	if testSummary := r.formatTestSummary(tests, scriptErr); testSummary != "" {
		summary = joinSections(summary, testSummary)
	}
	return summary
}

func (r responseRenderer) renderStatusLine(status string, code int) string {
	trimmed := strings.TrimSpace(status)
	if trimmed == "" {
		return ""
	}
	style := r.selectStatusStyle(code)
	return renderLabelValue("Status", trimmed, r.stats.Label, style)
}

type contentLen struct {
	n       int64
	raw     string
	has     bool
	numeric bool
}

func contentLength(resp *httpclient.Response) contentLen {
	if resp == nil {
		return contentLen{}
	}
	if resp.Headers != nil {
		if v := strings.TrimSpace(resp.Headers.Get("Content-Length")); v != "" {
			if n, err := strconv.ParseInt(v, 10, 64); err == nil && n >= 0 {
				return contentLen{n: n, has: true, numeric: true}
			}
			return contentLen{raw: v, has: true}
		}
	}
	n := int64(len(resp.Body))
	return contentLen{n: n, has: true, numeric: true}
}

func renderContentLengthLine(resp *httpclient.Response) string {
	return defaultResponseRenderer().renderContentLengthLine(resp)
}

func (r responseRenderer) renderContentLengthLine(resp *httpclient.Response) string {
	cl := contentLength(resp)
	if !cl.has {
		return ""
	}

	value := cl.raw
	if cl.numeric {
		value = formatByteQuantity(cl.n)
	}

	return renderLabelValue("Content-Length", value, r.stats.Label, r.stats.Value)
}

func renderContentLengthLinePretty(resp *httpclient.Response) string {
	return defaultResponseRenderer().renderContentLengthLinePretty(resp)
}

func (r responseRenderer) renderContentLengthLinePretty(resp *httpclient.Response) string {
	cl := contentLength(resp)
	if !cl.has {
		return ""
	}

	value := cl.raw
	if cl.numeric {
		value = formatByteSize(cl.n)
	}

	return renderLabelValue("Content-Length", value, r.stats.Label, r.stats.Value)
}

func formatByteQuantity(n int64) string {
	return bodyfmt.FormatByteQuantity(n)
}

func formatByteSize(n int64) string {
	return bodyfmt.FormatByteSize(n)
}

func (r responseRenderer) selectStatusStyle(code int) lipgloss.Style {
	switch statusLevelForHTTPStatus(code) {
	case statusSuccess:
		return r.stats.Success
	case statusWarn:
		return r.stats.Caution
	case statusError:
		return r.stats.Warn
	default:
		return r.stats.Value
	}
}

func joinSections(sections ...string) string {
	var parts []string
	for _, section := range sections {
		trimmed := trimSection(section)
		if strings.TrimSpace(trimmed) == "" {
			continue
		}
		parts = append(parts, trimmed)
	}
	return strings.Join(parts, "\n\n")
}

func trimSection(section string) string {
	if section == "" {
		return ""
	}
	return strings.Trim(section, "\r\n")
}

func makeReadOnlyKeyMap(base textarea.KeyMap) textarea.KeyMap {
	read := base
	read.DeleteAfterCursor = key.Binding{}
	read.DeleteBeforeCursor = key.Binding{}
	read.DeleteCharacterBackward = key.Binding{}
	read.DeleteCharacterForward = key.Binding{}
	read.DeleteWordBackward = key.Binding{}
	read.DeleteWordForward = key.Binding{}
	read.InsertNewline = key.Binding{}
	read.Paste = key.Binding{}
	read.LowercaseWordForward = key.Binding{}
	read.UppercaseWordForward = key.Binding{}
	read.CapitalizeWordForward = key.Binding{}
	read.TransposeCharacterBackward = key.Binding{}
	return read
}

func trimTrailingNewline(content string) string {
	if content == "" {
		return content
	}
	end := len(content)
	if end == 0 || content[end-1] != '\n' {
		return content
	}
	end--
	if end > 0 && content[end-1] == '\r' {
		end--
	}
	return content[:end]
}

func trimSyntheticNewline(content string, syn bool) string {
	if !syn {
		return content
	}
	return trimTrailingNewline(content)
}

func wrapToWidth(content string, width int) string {
	out, _ := wrapToWidthCtx(context.Background(), content, width)
	return out
}

func wrapToWidthCtx(ctx context.Context, content string, width int) (string, bool) {
	res, ok := wrap.Wrap(ctx, content, width, wrap.Plain, false)
	if !ok {
		return "", false
	}
	return res.S, true
}

func wrapContentForTab(tab responseTab, content string, width int) string {
	out, _ := wrapContentForTabCtx(context.Background(), tab, content, width)
	return out
}

func wrapContentForTabCtx(
	ctx context.Context,
	tab responseTab,
	content string,
	width int,
) (string, bool) {
	switch tab {
	case responseTabDiff:
		return wrapDiffContentCtx(ctx, content, width)
	case responseTabRaw:
		res, ok := wrap.Wrap(ctx, content, width, wrap.Pre, false)
		if !ok {
			return "", false
		}
		return res.S, true
	case responseTabPretty:
		res, ok := wrap.Wrap(ctx, content, width, wrap.Structured, false)
		if !ok {
			return "", false
		}
		return res.S, true
	default:
		res, ok := wrap.Wrap(ctx, content, width, wrap.Plain, false)
		if !ok {
			return "", false
		}
		return res.S, true
	}
}

func wrapContentForTabMapCtx(
	ctx context.Context,
	tab responseTab,
	content string,
	width int,
) (string, []lineSpan, []int, bool) {
	mode := wrap.Plain
	switch tab {
	case responseTabRaw:
		mode = wrap.Pre
	case responseTabPretty:
		mode = wrap.Structured
	}
	res, ok := wrap.Wrap(ctx, content, width, mode, true)
	if !ok {
		return "", nil, nil, false
	}
	spans := make([]lineSpan, len(res.Sp))
	for i, sp := range res.Sp {
		spans[i] = lineSpan{start: sp.S, end: sp.E}
	}
	return res.S, spans, res.Rv, true
}

func wrapCache(tab responseTab, content string, width int) cachedWrap {
	cache, _ := wrapCacheCtx(context.Background(), tab, content, width)
	return cache
}

func wrapCacheCtx(
	ctx context.Context,
	tab responseTab,
	content string,
	width int,
) (cachedWrap, bool) {
	if ctxDone(ctx) {
		return cachedWrap{}, false
	}
	if !respTabSel(tab) {
		wrapped, ok := wrapContentForTabCtx(ctx, tab, content, width)
		if !ok {
			return cachedWrap{}, false
		}
		return cachedWrap{
			width:   width,
			content: wrapped,
			valid:   true,
		}, true
	}
	wrapped, spans, rev, ok := wrapContentForTabMapCtx(ctx, tab, content, width)
	if !ok {
		return cachedWrap{}, false
	}
	return cachedWrap{
		width:   width,
		content: wrapped,
		valid:   true,
		spans:   spans,
		rev:     rev,
	}, true
}

func wrapPreformattedContent(content string, width int) string {
	out, _ := wrapPreformattedContentCtx(context.Background(), content, width)
	return out
}

func wrapPreformattedContentCtx(ctx context.Context, content string, width int) (string, bool) {
	res, ok := wrap.Wrap(ctx, content, width, wrap.Pre, false)
	if !ok {
		return "", false
	}
	return res.S, true
}

func leadingIndent(line string) string {
	if line == "" {
		return ""
	}

	var builder strings.Builder
	for _, r := range line {
		if r == ' ' || r == '\t' {
			builder.WriteRune(r)
			continue
		}
		break
	}
	return builder.String()
}

func wrapLineSegments(line string, width int) []string {
	segments, _ := wrapLineSegmentsCtx(context.Background(), line, width)
	return segments
}

func wrapLineSegmentsCtx(ctx context.Context, line string, width int) ([]string, bool) {
	return wrap.Line(ctx, line, width, wrap.Plain)
}

func centerContent(content string, width, height int) string {
	if content == "" {
		return ""
	}

	lines := strings.Split(content, "\n")
	trimmed := make([]string, len(lines))
	maxWidth := 0
	for i, line := range lines {
		trimmedLine := strings.TrimRight(line, " ")
		trimmed[i] = trimmedLine
		if w := visibleWidth(trimmedLine); w > maxWidth {
			maxWidth = w
		}
	}

	if width <= 0 {
		width = maxWidth
	}

	padded := make([]string, len(trimmed))
	for i, line := range trimmed {
		lineWidth := visibleWidth(line)
		if width <= lineWidth {
			padded[i] = line
			continue
		}

		padding := (width - lineWidth) / 2
		if padding < 0 {
			padding = 0
		}
		padded[i] = strings.Repeat(" ", padding) + line
	}

	if height > len(padded) {
		topPadding := (height - len(padded)) / 2
		if topPadding > 0 {
			blank := make([]string, topPadding)
			padded = append(blank, padded...)
		}
	}

	return strings.Join(padded, "\n")
}

func visibleWidth(s string) int {
	if s == "" {
		return 0
	}
	clean := ansiSequenceRegex.ReplaceAllString(s, "")
	return runewidth.StringWidth(clean)
}

func currentCursorLine(ed requestEditor) int {
	return ed.Line() + 1
}

func requestAtLine(doc *restfile.Document, line int) (*restfile.Request, int) {
	if doc == nil || line < 1 {
		return nil, -1
	}

	for idx, req := range doc.Requests {
		if line >= req.LineRange.Start && line <= req.LineRange.End {
			return req, idx
		}
	}

	// parser anchors LineRange.Start at the first non header line of a request.
	// treat the preceding line (should always be "###" separator) as part of the request
	// so the cursor on the header selects the correct request in the UI.
	headerLine := line + 1
	for idx, req := range doc.Requests {
		if headerLine == req.LineRange.Start {
			return req, idx
		}
	}

	return nil, -1
}

func requestIdentifier(req *restfile.Request) string {
	if req == nil {
		return ""
	}
	if req.Metadata.Name != "" {
		return req.Metadata.Name
	}
	return strings.TrimSpace(req.URL)
}

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}
