package replay

import (
	"bytes"
	"compress/gzip"
	"compress/zlib"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"io"
	"log"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	"charm.land/bubbles/v2/key"
	"charm.land/bubbles/v2/viewport"
	tea "charm.land/bubbletea/v2"
	"charm.land/lipgloss/v2"
	"github.com/andybalholm/brotli"
	ilovetui "github.com/anotherhadi/ilovetui"
	"github.com/anotherhadi/spilltea/internal/config"
	"github.com/anotherhadi/spilltea/internal/db"
	"github.com/anotherhadi/spilltea/internal/keys"
	"github.com/anotherhadi/spilltea/internal/style"
	diffUI "github.com/anotherhadi/spilltea/internal/ui/diff"
	"github.com/anotherhadi/spilltea/internal/util"
	"github.com/klauspost/compress/zstd"
)

type sentMsg struct {
	index       int
	responseRaw string
	statusCode  int
	err         error
}

func sendCmd(entry Entry, index int) tea.Cmd {
	return func() tea.Msg {
		raw, code, err := doSend(entry)
		return sentMsg{index: index, responseRaw: raw, statusCode: code, err: err}
	}
}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	var cmds []tea.Cmd

	// Route non-key messages to textarea when editing so internal
	// textarea messages (e.g. clipboard paste) are handled correctly.
	if m.editing {
		if _, ok := msg.(tea.KeyPressMsg); !ok {
			var taCmd tea.Cmd
			m.textarea, taCmd = m.textarea.Update(msg)
			cmds = append(cmds, taCmd)
		}
	}

	switch msg := msg.(type) {
	case SendToReplayMsg:
		entry := entryFromMsg(msg)
		if m.database != nil {
			id, err := m.database.InsertReplayEntry(entryToDB(entry))
			if err == nil {
				entry.DBID = id
			}
		}
		m.allEntries = append(m.allEntries, entry)
		m.applyFilter(len(m.allEntries) - 1)

	case sentMsg:
		allIdx := msg.index
		if allIdx >= 0 && allIdx < len(m.allEntries) {
			e := m.allEntries[allIdx]
			e.Sending = false
			e.StatusCode = msg.statusCode
			e.ResponseRaw = msg.responseRaw
			if msg.err != nil {
				e.Err = msg.err
				e.ResponseRaw = "Error: " + msg.err.Error()
			}
			if m.database != nil && e.DBID != 0 {
				if err := m.database.UpdateReplayEntry(entryToDB(e)); err != nil {
					log.Printf("replay: update entry: %v", err)
				}
			}
			m.allEntries[allIdx] = e
		}
		prevAllIdx := m.currentAllIdx()
		m.applyFilter(prevAllIdx)

	case util.EditorFinishedMsg:
		if msg.Err == nil && msg.Content != "" && len(m.entries) > 0 {
			m.entries[m.cursor].RequestRaw = msg.Content
			m.refreshBody()
		}

	case tea.MouseWheelMsg:
		if !m.editing {
			switch msg.Button {
			case tea.MouseWheelUp:
				if msg.Mod.Contains(tea.ModShift) {
					m.scrollBothViewports(true)
				} else {
					m.scrollFocusedViewportVertical(-1)
				}
			case tea.MouseWheelDown:
				if msg.Mod.Contains(tea.ModShift) {
					m.scrollBothViewports(false)
				} else {
					m.scrollFocusedViewportVertical(1)
				}
			case tea.MouseWheelLeft:
				m.scrollBothViewports(true)
			case tea.MouseWheelRight:
				m.scrollBothViewports(false)
			}
		}

	case tea.KeyPressMsg:
		if m.editing {
			return m.updateEditMode(msg)
		}
		return m.updateNormalMode(msg)
	}

	return m, tea.Batch(cmds...)
}

func (m Model) updateNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	g := keys.Keys.Global
	r := keys.Keys.Replay

	if m.filterActive && !m.filterAccepted {
		switch {
		case key.Matches(msg, g.Escape):
			m.clearFilter()
		case msg.String() == "enter":
			m.filterAccepted = true
			m.filterInput.Blur()
			m.recalcSizes()
		default:
			var cmd tea.Cmd
			m.filterInput, cmd = m.filterInput.Update(msg)
			prevAllIdx := m.currentAllIdx()
			m.applyFilter(prevAllIdx)
			return m, cmd
		}
		return m, nil
	}

	if m.filterActive && m.filterAccepted {
		if key.Matches(msg, g.Escape) {
			m.clearFilter()
			return m, nil
		}
	}

	switch {
	case key.Matches(msg, g.Up):
		if m.focusedPanel == panelList {
			if m.cursor > 0 {
				m.cursor--
				m.refreshListViewport()
				m.refreshBody()
			}
		} else {
			m.scrollFocusedViewportVertical(-1)
		}

	case key.Matches(msg, g.Down):
		if m.focusedPanel == panelList {
			if m.cursor < len(m.entries)-1 {
				m.cursor++
				m.refreshListViewport()
				m.refreshBody()
			}
		} else {
			m.scrollFocusedViewportVertical(1)
		}

	case key.Matches(msg, g.CycleFocus):
		switch m.focusedPanel {
		case panelList:
			m.focusedPanel = panelRequest
		case panelRequest:
			m.focusedPanel = panelResponse
		default:
			m.focusedPanel = panelList
		}

	case key.Matches(msg, r.Send):
		if len(m.entries) > 0 && !m.entries[m.cursor].Sending {
			allIdx := m.currentAllIdx()
			m.allEntries[allIdx].Sending = true
			m.allEntries[allIdx].ResponseRaw = ""
			m.allEntries[allIdx].Err = nil
			entry := m.allEntries[allIdx]
			m.applyFilter(allIdx)
			return m, sendCmd(entry, allIdx)
		}

	case key.Matches(msg, r.Edit):
		if len(m.entries) > 0 {
			m.textarea.SetValue(m.entries[m.cursor].RequestRaw)
			m.editing = true
			m.textarea.Focus()
		}

	case key.Matches(msg, r.EditExt):
		if len(m.entries) > 0 {
			if m.focusedPanel == panelResponse {
				return m, util.OpenExternalEditorView(m.entries[m.cursor].ResponseRaw)
			}
			return m, util.OpenExternalEditor(m.entries[m.cursor].RequestRaw)
		}

	case key.Matches(msg, r.UndoEdits):
		if len(m.entries) > 0 {
			m.entries[m.cursor].RequestRaw = m.entries[m.cursor].OriginalRaw
			m.refreshBody()
		}

	case key.Matches(msg, g.ScrollUp):
		vp := m.focusedViewport()
		util.ScrollViewport(&vp, -1)
		m.setFocusedViewport(vp)

	case key.Matches(msg, g.ScrollDown):
		vp := m.focusedViewport()
		util.ScrollViewport(&vp, 1)
		m.setFocusedViewport(vp)

	case key.Matches(msg, g.Left):
		m.requestViewport.ScrollLeft(6)
		m.responseViewport.ScrollLeft(6)

	case key.Matches(msg, g.Right):
		m.requestViewport.ScrollRight(6)
		m.responseViewport.ScrollRight(6)

	case key.Matches(msg, r.Flag):
		if len(m.entries) > 0 {
			allIdx := m.currentAllIdx()
			if m.database != nil && m.allEntries[allIdx].DBID != 0 {
				if err := m.database.ToggleReplayFlag(m.allEntries[allIdx].DBID); err != nil {
					log.Printf("replay: toggle flag: %v", err)
				}
			}
			m.allEntries[allIdx].Flagged = !m.allEntries[allIdx].Flagged
			m.applyFilter(allIdx)
		}

	case key.Matches(msg, r.Delete):
		if len(m.entries) > 0 {
			allIdx := m.currentAllIdx()
			e := m.allEntries[allIdx]
			if m.database != nil && e.DBID != 0 {
				if err := m.database.DeleteReplayEntry(e.DBID); err != nil {
					log.Printf("replay: delete entry: %v", err)
				}
			}
			m.allEntries = append(m.allEntries[:allIdx], m.allEntries[allIdx+1:]...)
			preferAllIdx := allIdx
			if preferAllIdx >= len(m.allEntries) {
				preferAllIdx = len(m.allEntries) - 1
			}
			m.applyFilter(preferAllIdx)
		}

	case key.Matches(msg, r.DeleteAll):
		if m.database != nil {
			if err := m.database.DeleteAllReplayEntriesExceptFlagged(); err != nil {
				log.Printf("replay: delete all entries: %v", err)
			}
		}
		// Mirror the DB logic in memory: delete unflagged first; if none, delete all.
		hasUnflagged := false
		for _, e := range m.allEntries {
			if !e.Flagged {
				hasUnflagged = true
				break
			}
		}
		if hasUnflagged {
			filtered := m.allEntries[:0]
			for _, e := range m.allEntries {
				if e.Flagged {
					filtered = append(filtered, e)
				}
			}
			m.allEntries = filtered
		} else {
			m.allEntries = nil
		}
		m.cursor = 0
		m.filterActive = false
		m.filterAccepted = false
		m.filterInput.SetValue("")
		m.filterInput.Blur()
		m.recalcSizes()
		m.applyFilter(-1)

	case key.Matches(msg, keys.Keys.Global.GotoTop):
		m.cursor = 0
		m.pager.Page = 0
		m.refreshListViewport()
		m.refreshBody()

	case key.Matches(msg, keys.Keys.Global.GotoBottom):
		m.cursor = util.CursorGotoBottom(len(m.entries))
		m.refreshListViewport()
		m.refreshBody()

	case key.Matches(msg, keys.Keys.Global.PrevPage):
		m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, false)
		m.refreshListViewport()
		m.refreshBody()

	case key.Matches(msg, keys.Keys.Global.NextPage):
		m.cursor = util.CursorMovePage(m.cursor, len(m.entries), m.pager.PerPage, true)
		m.refreshListViewport()
		m.refreshBody()

	case key.Matches(msg, g.SendToDiff):
		if len(m.entries) > 0 {
			e := m.entries[m.cursor]
			var raw, label string
			if m.focusedPanel == panelResponse {
				raw = e.ResponseRaw
				label = fmt.Sprintf("%d %s", e.StatusCode, http.StatusText(e.StatusCode))
			} else {
				raw = e.RequestRaw
				label = e.Method + " " + e.Host + e.Path
			}
			if raw != "" {
				return m, func() tea.Msg {
					return diffUI.SendToDiffMsg{Label: label, Raw: raw}
				}
			}
		}

	case key.Matches(msg, r.Filter):
		if m.focusedPanel == panelList {
			m.filterActive = true
			m.filterAccepted = false
			m.filterInput.Placeholder = "filter requests..."
			m.filterInput.Focus()
			m.recalcSizes()
		}

	case key.Matches(msg, g.Help):
		m.help.ShowAll = !m.help.ShowAll
		m.recalcSizes()
	}

	return m, nil
}

func (m Model) updateEditMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
	switch {
	case key.Matches(msg, keys.Keys.Global.Escape):
		if len(m.entries) > 0 {
			m.entries[m.cursor].RequestRaw = m.textarea.Value()
		}
		m.editing = false
		m.textarea.Blur()
		m.refreshBody()

	default:
		var cmd tea.Cmd
		m.textarea, cmd = m.textarea.Update(msg)
		return m, cmd
	}

	return m, nil
}

// focusedViewport returns the viewport that should receive scroll events.
// When the list is focused, scroll targets the request panel.
func (m *Model) focusedViewport() viewport.Model {
	if m.focusedPanel == panelResponse {
		return m.responseViewport
	}
	return m.requestViewport
}

func (m *Model) setFocusedViewport(vp viewport.Model) {
	if m.focusedPanel == panelResponse {
		m.responseViewport = vp
	} else {
		m.requestViewport = vp
	}
}

func (m *Model) scrollBothViewports(left bool) {
	if left {
		m.requestViewport.ScrollLeft(6)
		m.responseViewport.ScrollLeft(6)
	} else {
		m.requestViewport.ScrollRight(6)
		m.responseViewport.ScrollRight(6)
	}
}

func (m *Model) scrollFocusedViewportVertical(delta int) {
	vp := m.focusedViewport()
	vp.SetYOffset(vp.YOffset() + delta)
	m.setFocusedViewport(vp)
}

func (m *Model) refreshListViewport() {
	if m.pager.PerPage > 0 {
		if len(m.entries) == 0 {
			m.pager.Page = 0
			m.pager.TotalPages = 0
		} else {
			m.pager.Page = m.cursor / m.pager.PerPage
			m.pager.SetTotalPages(len(m.entries))
		}
	}
	m.listViewport.SetContent(m.renderList())
}

func (m *Model) refreshBody() {
	if len(m.entries) == 0 {
		m.requestViewport.SetContent("")
		m.responseViewport.SetContent("")
		return
	}
	e := m.entries[m.cursor]
	m.requestViewport.SetContent(style.HighlightHTTP(e.RequestRaw))
	m.requestViewport.SetYOffset(0)
	m.requestViewport.SetXOffset(0)

	if e.Sending {
		m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.responseViewport.Width(), "(ﾉ◕ヮ◕)ﾉ*:･ﾟ", "sending..."))))
	} else if e.ResponseRaw != "" {
		m.responseViewport.SetContent(style.HighlightHTTP(e.ResponseRaw))
	} else {
		m.responseViewport.SetContent(lipgloss.Place(m.responseViewport.Width(), m.responseViewport.Height(), lipgloss.Center, lipgloss.Center, ilovetui.S.Faint.Render(util.EmptyState(m.responseViewport.Width(), "( •_•)>⌐■", "press send to fire"))))
	}
	m.responseViewport.SetYOffset(0)
	m.responseViewport.SetXOffset(0)
}

func doSend(entry Entry) (responseRaw string, statusCode int, err error) {
	parsed := util.ParseRawRequest(entry.RequestRaw)
	if parsed.Method == "" {
		return "", 0, fmt.Errorf("empty request")
	}

	host := parsed.Host
	if host == "" {
		host = entry.Host
	}
	headers := make(http.Header)
	for _, h := range parsed.Headers {
		if strings.EqualFold(h.Key, "host") {
			continue
		}
		headers.Add(h.Key, h.Value)
	}

	scheme := entry.Scheme
	if scheme == "" {
		scheme = "https"
	}
	urlStr := scheme + "://" + host + parsed.Path

	var bodyReader io.Reader
	if parsed.Body != "" {
		bodyReader = strings.NewReader(parsed.Body)
	}

	req, err := http.NewRequest(parsed.Method, urlStr, bodyReader)
	if err != nil {
		return "", 0, err
	}
	req.Header = headers

	tlsCfg := &tls.Config{InsecureSkipVerify: config.Global.App.SslInsecure} // #nosec G402 -- controlled by ssl_insecure config
	if !config.Global.App.SslInsecure {
		if certPool, err := x509.SystemCertPool(); err == nil {
			caPath := filepath.Join(config.ExpandPath(config.Global.App.CertDir), "mitmproxy-ca-cert.pem")
			if pem, err := os.ReadFile(caPath); err == nil {
				certPool.AppendCertsFromPEM(pem)
			}
			tlsCfg.RootCAs = certPool
		}
	}
	transport := &http.Transport{
		TLSClientConfig: tlsCfg,
	}
	if up := config.Global.App.UpstreamProxy; up != "" {
		if proxyURL, err := url.Parse(up); err == nil {
			transport.Proxy = http.ProxyURL(proxyURL)
		}
	}
	client := &http.Client{
		Timeout:   30 * time.Second,
		Transport: transport,
		CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
			return http.ErrUseLastResponse
		},
	}

	resp, err := client.Do(req)
	if err != nil {
		return "", 0, err
	}
	defer resp.Body.Close()

	limit := int64(config.Global.App.MaxBodySizeMB) * 1024 * 1024
	respBody, _ := io.ReadAll(io.LimitReader(resp.Body, limit))

	if enc := resp.Header.Get("Content-Encoding"); enc != "" {
		if decoded, decErr := decodeBody(enc, respBody); decErr == nil {
			respBody = decoded
			resp.Header.Del("Content-Encoding")
			resp.Header.Del("Content-Length")
		}
	}

	var sb strings.Builder
	fmt.Fprintf(&sb, "%s %d %s\n", resp.Proto, resp.StatusCode, http.StatusText(resp.StatusCode))
	for _, line := range util.SortedHeaderLines(resp.Header) {
		sb.WriteString(line)
	}
	sb.WriteString("\n")
	sb.Write(respBody)

	return sb.String(), resp.StatusCode, nil
}

func decodeBody(encoding string, body []byte) ([]byte, error) {
	switch strings.ToLower(strings.TrimSpace(encoding)) {
	case "gzip":
		r, err := gzip.NewReader(bytes.NewReader(body))
		if err != nil {
			return nil, err
		}
		defer r.Close()
		return io.ReadAll(r)
	case "br":
		return io.ReadAll(brotli.NewReader(bytes.NewReader(body)))
	case "deflate":
		r, err := zlib.NewReader(bytes.NewReader(body))
		if err != nil {
			return nil, err
		}
		defer r.Close()
		return io.ReadAll(r)
	case "zstd":
		r, err := zstd.NewReader(bytes.NewReader(body))
		if err != nil {
			return nil, err
		}
		defer r.Close()
		return io.ReadAll(r)
	}
	return nil, fmt.Errorf("unsupported encoding: %s", encoding)
}

func entryToDB(e Entry) db.ReplayEntry {
	errMsg := ""
	if e.Err != nil {
		errMsg = e.Err.Error()
	}
	return db.ReplayEntry{
		ID:          e.DBID,
		Timestamp:   time.Now(),
		Scheme:      e.Scheme,
		Host:        e.Host,
		Path:        e.Path,
		Method:      e.Method,
		OriginalRaw: e.OriginalRaw,
		RequestRaw:  e.RequestRaw,
		ResponseRaw: e.ResponseRaw,
		StatusCode:  e.StatusCode,
		ErrorMsg:    errMsg,
		Flagged:     e.Flagged,
	}
}

func entryFromMsg(msg SendToReplayMsg) Entry {
	parsed := util.ParseRawRequest(msg.RequestRaw)
	host := parsed.Host
	if host == "" {
		host = msg.Host
	}
	scheme := msg.Scheme
	if scheme == "" {
		scheme = util.InferScheme(host)
	}
	return Entry{
		Scheme:      scheme,
		Host:        host,
		Path:        parsed.Path,
		Method:      parsed.Method,
		OriginalRaw: msg.RequestRaw,
		RequestRaw:  msg.RequestRaw,
	}
}
