//go:build unix

package main

import (
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"strings"
	"sync"
	"syscall"
	"unicode/utf8"
	"time"
	"unsafe"

	tea "charm.land/bubbletea/v2"
	cblog "github.com/charmbracelet/log"
	"github.com/creack/pty"
	"golang.org/x/sys/unix"
	"golang.org/x/term"
)

// openK9s runs k9s in a PTY with a status bar at the bottom showing Argonaut context
func (m *Model) openK9s(params K9sResourceParams) tea.Cmd {
	return func() tea.Msg {
		if m.program != nil {
			m.program.Send(pauseRenderingMsg{})
			_ = m.program.ReleaseTerminal()
		}
		defer func() {
			// Clear screen and restore terminal to Bubble Tea
			fmt.Print("\x1b[2J\x1b[H")
			time.Sleep(150 * time.Millisecond)
			if m.program != nil {
				_ = m.program.RestoreTerminal()
				m.program.Send(resumeRenderingMsg{})
			}
		}()

		// Check if k9s is available
		k9sCmd := m.config.GetK9sCommand()
		if !inPath(k9sCmd) {
			cblog.With("component", "k9s").Error("k9s not found in PATH")
			return k9sDoneMsg{Err: fmt.Errorf("k9s not found in PATH")}
		}

		// Map the kind to k9s resource alias
		resourceAlias := params.Kind
		if alias, ok := k9sResourceMap[params.Kind]; ok {
			resourceAlias = alias
		} else {
			resourceAlias = strings.ToLower(params.Kind)
		}

		// Build args - include filter if name is provided
		var args []string
		if params.Name != "" {
			args = []string{"-c", fmt.Sprintf("%s /%s", resourceAlias, params.Name)}
		} else {
			args = []string{"-c", resourceAlias}
		}
		if params.Namespace != "" {
			args = append(args, "-n", params.Namespace)
		}

		// Allow context override via config
		context := params.Context
		if cfgCtx := m.config.GetK9sContext(); cfgCtx != "" {
			context = cfgCtx
		}
		if context != "" {
			args = append(args, "--context", context)
		}

		cblog.With("component", "k9s").Info("Launching k9s sandboxed", "args", args)

		// Get terminal size
		rows, cols := getTerminalSize()
		if rows < 3 {
			rows = 24
		}
		if cols < 10 {
			cols = 80
		}

		// Reserve 1 row for our status bar at the bottom
		k9sRows := rows - 1

		// Create PTY for k9s with reduced height
		cmd := exec.Command(k9sCmd, args...)
		cmd.Env = os.Environ()

		ptmx, err := pty.StartWithSize(cmd, &pty.Winsize{
			Rows: uint16(k9sRows),
			Cols: uint16(cols),
		})
		if err != nil {
			cblog.With("component", "k9s").Error("Failed to start k9s PTY", "err", err)
			return k9sDoneMsg{Err: err}
		}
		// Shared state for current terminal size
		var sizeMu sync.Mutex
		currentRows := rows
		currentCols := cols

		// Handle window resize
		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, syscall.SIGWINCH)
		defer signal.Stop(sigCh)
		defer close(sigCh) // Close channel to allow goroutine to exit

		go func() {
			for range sigCh {
				newRows, newCols := getTerminalSize()
				if newRows >= 3 && newCols >= 10 {
					sizeMu.Lock()
					currentRows = newRows
					currentCols = newCols
					sizeMu.Unlock()

					k9sRows := newRows - 1
					_ = pty.Setsize(ptmx, &pty.Winsize{
						Rows: uint16(k9sRows),
						Cols: uint16(newCols),
					})
				}
			}
		}()

		// Put stdin into raw mode so keystrokes are passed through immediately
		oldState, err := makeRaw(int(os.Stdin.Fd()))
		if err != nil {
			cblog.With("component", "k9s").Error("Failed to set raw mode", "err", err)
			return k9sDoneMsg{Err: err}
		}
		defer restoreTerminal(int(os.Stdin.Fd()), oldState)

		// Clear screen and draw initial status bar
		fmt.Print("\x1b[2J\x1b[H")
		drawStatusBarBottom(rows, cols, params)

		// Set up input forwarding from stdin to the PTY using
		// unix.Select with a cancellation pipe. This lets us unblock
		// the goroutine deterministically after k9s exits without
		// modifying fd flags (SetNonblock corrupts the shared file
		// description) or relying on Go's poller (SetReadDeadline
		// doesn't work on dup'd terminal fds on macOS).
		cancelR, cancelW, err := os.Pipe()
		if err != nil {
			cblog.With("component", "k9s").Error("Failed to create cancel pipe", "err", err)
			ptmx.Close()
			return k9sDoneMsg{Err: fmt.Errorf("failed to create cancel pipe: %w", err)}
		}

		stdinDone := make(chan struct{})
		go func() {
			defer close(stdinDone)
			defer cancelR.Close()
			stdinFd := int(os.Stdin.Fd())
			cancelFd := int(cancelR.Fd())
			buf := make([]byte, 1024)
			for {
				rfds := &unix.FdSet{}
				rfds.Set(stdinFd)
				rfds.Set(cancelFd)
				maxFd := stdinFd
				if cancelFd > maxFd {
					maxFd = cancelFd
				}
				_, err := unix.Select(maxFd+1, rfds, nil, nil, nil)
				if err != nil {
					if err == syscall.EINTR {
						continue
					}
					return
				}
				if rfds.IsSet(cancelFd) {
					return
				}
				if rfds.IsSet(stdinFd) {
					n, err := syscall.Read(stdinFd, buf)
					if err != nil || n <= 0 {
						return
					}
					if _, err := ptmx.Write(buf[:n]); err != nil {
						return
					}
				}
			}
		}()

		// Process k9s output and inject status bar at frame boundaries
		processK9sOutputWithStatusBar(ptmx, &sizeMu, &currentRows, &currentCols, params)

		// Wait for k9s to exit
		if err := cmd.Wait(); err != nil {
			cblog.With("component", "k9s").Debug("k9s exited", "err", err)
		}

		// Close the cancel pipe to unblock the goroutine's select(),
		// then wait for it to exit deterministically.
		ptmx.Close()
		cancelW.Close()
		<-stdinDone

		return k9sDoneMsg{Err: nil}
	}
}

// processK9sOutputWithStatusBar reads k9s output and injects status bar at frame boundaries
func processK9sOutputWithStatusBar(ptmx *os.File, sizeMu *sync.Mutex, rows, cols *int, params K9sResourceParams) {
	buf := make([]byte, 32*1024)

	for {
		n, err := ptmx.Read(buf)
		if n > 0 {
			data := buf[:n]

			// Get current terminal size
			sizeMu.Lock()
			r, c := *rows, *cols
			sizeMu.Unlock()

			// Look for frame boundaries and inject status bar
			output := injectStatusBarAtFrameBoundaries(data, r, c, params)

			os.Stdout.Write(output)
		}
		if err != nil {
			break
		}
	}
}

// injectStatusBarAtFrameBoundaries finds frame boundary sequences and injects status bar after them
func injectStatusBarAtFrameBoundaries(data []byte, rows, cols int, params K9sResourceParams) []byte {
	// Build the status bar injection sequence
	statusBar := buildStatusBarSequence(rows, cols, params)

	// Patterns that indicate frame boundaries:
	// ESC[2J - clear entire screen (clears our status bar too!)
	// ESC[H - cursor home
	// ESC[;H - cursor home (explicit form)
	// ESC[1;1H - cursor to row 1, col 1
	clearScreen := []byte("\x1b[2J")
	cursorHome := []byte("\x1b[H")
	cursorHomeExplicit := []byte("\x1b[;H")
	cursorHome11 := []byte("\x1b[1;1H")

	var result bytes.Buffer
	i := 0

	for i < len(data) {
		matched := false

		// Check for clear screen - inject status bar AFTER it
		if i+len(clearScreen) <= len(data) && bytes.Equal(data[i:i+len(clearScreen)], clearScreen) {
			result.Write(clearScreen)
			result.Write(statusBar)
			i += len(clearScreen)
			matched = true
		}

		// Check for cursor home patterns - inject status bar BEFORE them (end of previous frame)
		if !matched {
			for _, pattern := range [][]byte{cursorHome11, cursorHomeExplicit, cursorHome} {
				if i+len(pattern) <= len(data) && bytes.Equal(data[i:i+len(pattern)], pattern) {
					result.Write(statusBar)
					result.Write(pattern)
					i += len(pattern)
					matched = true
					break
				}
			}
		}

		if !matched {
			result.WriteByte(data[i])
			i++
		}
	}

	return result.Bytes()
}

// buildStatusBarSequence creates the ANSI sequence to draw the status bar on the last row
func buildStatusBarSequence(rows, cols int, params K9sResourceParams) []byte {
	var buf bytes.Buffer

	// Save cursor, move to last row, clear line
	buf.WriteString("\x1b7")                      // Save cursor
	buf.WriteString(fmt.Sprintf("\x1b[%d;1H", rows)) // Move to last row
	buf.WriteString("\x1b[2K")                    // Clear line

	// Build status bar content
	left := " Argonaut » k9s"
	right := ":q to return "

	// Calculate padding using rune count (» is multi-byte UTF-8)
	padding := cols - utf8.RuneCountInString(left) - utf8.RuneCountInString(right)
	if padding < 1 {
		padding = 1
	}

	// Draw with colors: blue background (#0087af = 256-color 31), white text
	buf.WriteString("\x1b[48;5;31;97m")
	buf.WriteString(left)
	buf.WriteString(strings.Repeat(" ", padding))
	buf.WriteString(right)
	buf.WriteString("\x1b[0m")

	// Restore cursor
	buf.WriteString("\x1b8")

	return buf.Bytes()
}

// drawStatusBarBottom draws the status bar on the last row of the terminal (for initial draw)
func drawStatusBarBottom(rows, cols int, params K9sResourceParams) {
	os.Stdout.Write(buildStatusBarSequence(rows, cols, params))
}

// getTerminalSize returns the current terminal rows and cols
func getTerminalSize() (int, int) {
	ws := struct {
		Row uint16
		Col uint16
		X   uint16
		Y   uint16
	}{}

	_, _, _ = syscall.Syscall(
		syscall.SYS_IOCTL,
		os.Stdout.Fd(),
		uintptr(syscall.TIOCGWINSZ),
		uintptr(unsafe.Pointer(&ws)),
	)

	return int(ws.Row), int(ws.Col)
}

// makeRaw puts the terminal into raw mode and returns the previous state
func makeRaw(fd int) (*term.State, error) {
	return term.MakeRaw(fd)
}

// restoreTerminal restores the terminal to a previous state
func restoreTerminal(fd int, state *term.State) {
	if state != nil {
		_ = term.Restore(fd, state)
	}
}
