package tui

import (
	"albedo/cli/internal/daemon"
	"bytes"
	"context"
	"encoding/base64"
	"encoding/hex"
	"errors"
	"fmt"
	"image"
	_ "image/jpeg"
	_ "image/png"
	"io"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	tea "charm.land/bubbletea/v2"
	"github.com/atotto/clipboard"
)

const (
	MaxClipboardImageBytes = 5 * 1024 * 1024
	MaxClipboardImageEdge  = 16384
)

func execClipboardCommand(ctx context.Context, name string, args ...string) ([]byte, error) {
	cmd := exec.CommandContext(ctx, name, args...)
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		return nil, err
	}
	defer stdout.Close()
	if err = cmd.Start(); err != nil {
		return nil, err
	}
	// Killing the command does not close stdout held open by a descendant.
	stopClosingStdout := context.AfterFunc(ctx, func() { _ = stdout.Close() })
	defer stopClosingStdout()

	// The extra byte detects oversized output without buffering the rest.
	limited := io.LimitReader(stdout, MaxClipboardImageBytes+1)
	buf, err := io.ReadAll(limited)
	if err != nil || len(buf) > MaxClipboardImageBytes {
		// Once reading stops, a child still writing to the pipe could block Wait.
		_ = cmd.Process.Kill()
	}
	waitErr := cmd.Wait()

	if len(buf) > MaxClipboardImageBytes {
		return nil, fmt.Errorf("clipboard image exceeds %d byte limit", MaxClipboardImageBytes)
	}
	if ctx.Err() != nil {
		return buf, ctx.Err()
	}
	if err != nil {
		return buf, err
	}
	return buf, waitErr
}

// CopyText puts text on the clipboard of the machine albedo runs on and asks
// the terminal to put it on its own with OSC 52. Those differ when you reach
// albedo over ssh or attach to its session from another device, and only the
// terminal's is the one you paste from; a terminal without OSC 52 still has
// the local copy.
func CopyText(text string) tea.Cmd {
	return tea.Batch(tea.SetClipboard(text), func() tea.Msg {
		_ = clipboard.WriteAll(text)
		return nil
	})
}

func isRemote() bool {
	return os.Getenv("SSH_CONNECTION") != "" || os.Getenv("SSH_CLIENT") != "" || os.Getenv("MOSH_CONNECTION") != ""
}

func ClipboardHasImage() bool {
	if isRemote() {
		return false
	}

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	switch runtime.GOOS {
	case "darwin":
		if _, err := exec.LookPath("pngpaste"); err == nil {
			if out, err := execClipboardCommand(ctx, "pngpaste", "-b"); err == nil && len(out) == 0 {
				return true
			}
		}
		out, err := execClipboardCommand(ctx, "osascript", "-e", "clipboard info")
		if err != nil {
			return false
		}
		s := string(out)
		return strings.Contains(s, "«class PNGf»") || strings.Contains(s, "JPEG picture") || strings.Contains(s, "TIFF picture")

	case "linux":
		// Wayland first: under a compositor both may be set.
		probes := []struct {
			env, tool string
			args      []string
		}{
			{"WAYLAND_DISPLAY", "wl-paste", []string{"--list-types"}},
			{"DISPLAY", "xclip", []string{"-selection", "clipboard", "-t", "TARGETS", "-o"}},
		}
		for _, probe := range probes {
			if os.Getenv(probe.env) == "" {
				continue
			}
			if _, err := exec.LookPath(probe.tool); err != nil {
				continue
			}
			out, err := execClipboardCommand(ctx, probe.tool, probe.args...)
			if err == nil && strings.Contains(string(out), "image/") {
				return true
			}
		}
		return false
	}

	return false
}

func parseAppleScriptHex(out []byte) ([]byte, error) {
	s, ok1 := strings.CutPrefix(strings.TrimSpace(string(out)), "«data ")
	s, ok2 := strings.CutSuffix(s, "»")
	if !ok1 || !ok2 {
		return nil, errors.New("not an AppleScript hex payload")
	}
	fields := strings.Fields(s)
	if len(fields) == 0 {
		return nil, errors.New("empty AppleScript data")
	}
	// Skip the 4-char ostype code (e.g. PNGf, JPEG)
	if len(fields[0]) <= 4 {
		return nil, errors.New("missing hex payload in AppleScript data")
	}
	return hex.DecodeString(fields[0][4:])
}

func ReadClipboardImage() (*daemon.ImageAttachment, error) {
	if isRemote() {
		return nil, errors.New("remote clipboard image unsupported")
	}

	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	var data []byte
	var err error

	switch runtime.GOOS {
	case "darwin":
		if _, errPath := exec.LookPath("pngpaste"); errPath == nil {
			data, err = execClipboardCommand(ctx, "pngpaste", "-")
		} else {
			rawOut, errCmd := execClipboardCommand(ctx, "osascript", "-e", "get the clipboard as «class PNGf»")
			if errCmd != nil {
				return nil, errCmd
			}
			if len(rawOut) > MaxClipboardImageBytes {
				return nil, fmt.Errorf("clipboard image too large: %d bytes (max %d)", len(rawOut), MaxClipboardImageBytes)
			}
			data, err = parseAppleScriptHex(rawOut)
		}

	case "linux":
		if os.Getenv("WAYLAND_DISPLAY") != "" {
			data, err = execClipboardCommand(ctx, "wl-paste", "-t", "image/png")
		} else if os.Getenv("DISPLAY") != "" {
			data, err = execClipboardCommand(ctx, "xclip", "-selection", "clipboard", "-t", "image/png", "-o")
		} else {
			return nil, errors.New("no display or clipboard tool available")
		}

	default:
		return nil, fmt.Errorf("clipboard images are not supported on %s", runtime.GOOS)
	}

	if err != nil {
		return nil, fmt.Errorf("could not read clipboard image: %w", err)
	}

	if len(data) == 0 {
		return nil, errors.New("clipboard contains no image data")
	}

	if len(data) > MaxClipboardImageBytes {
		return nil, fmt.Errorf("clipboard image too large: %d bytes (max %d)", len(data), MaxClipboardImageBytes)
	}

	// Strictly validate config - rejects truncated or 0-dimension images
	cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
	if err != nil {
		return nil, fmt.Errorf("clipboard image is invalid or incomplete: %w", err)
	}
	if cfg.Width <= 0 || cfg.Height <= 0 {
		return nil, errors.New("invalid image dimensions: zero or negative")
	}
	if cfg.Width > MaxClipboardImageEdge || cfg.Height > MaxClipboardImageEdge {
		return nil, fmt.Errorf("image dimensions too large: %dx%d (max edge %d)", cfg.Width, cfg.Height, MaxClipboardImageEdge)
	}

	mime := daemon.ImagePNG
	switch format {
	case "jpeg", "jpg":
		mime = daemon.ImageJPEG
	case "webp":
		mime = daemon.ImageWEBP
	}

	encoded := base64.StdEncoding.EncodeToString(data)
	return &daemon.ImageAttachment{
		ImageMetadata: daemon.ImageMetadata{
			MimeType: mime,
			Width:    cfg.Width,
			Height:   cfg.Height,
			Bytes:    len(data),
		},
		Data: encoded,
	}, nil
}

type ClipboardImagePastedMsg struct {
	Err        error
	Image      *daemon.ImageAttachment
	SessionID  string
	Generation int64
}

func PasteClipboardImageCmd(sessionID string, gen int64) tea.Cmd {
	return func() tea.Msg {
		if !ClipboardHasImage() {
			return ClipboardImagePastedMsg{SessionID: sessionID, Generation: gen}
		}
		img, err := ReadClipboardImage()
		return ClipboardImagePastedMsg{SessionID: sessionID, Generation: gen, Image: img, Err: err}
	}
}
