package ugh

import (
	"context"
	"fmt"
	"io"
	"strings"
	"time"
)

type step interface {
	execute(ctx context.Context, c *Console, appInput io.Writer) error
	String() string
}

type expectMatcherStep struct {
	matcher Matcher
	timeout time.Duration
}

func (s *expectMatcherStep) execute(ctx context.Context, c *Console, _ io.Writer) error {
	deadline := time.After(s.timeout)
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		screen := c.Screen()
		if s.matcher.Match(screen) {
			return nil
		}

		select {
		case <-ctx.Done():
			return fmt.Errorf("ugh: context cancelled while waiting for %q\n\nScreen:\n%s", s.matcher, c.Screen())
		case <-deadline:
			return fmt.Errorf("ugh: timeout waiting for %q\n\nScreen:\n%s", s.matcher, c.Screen())
		case <-ticker.C:
		}
	}
}

func (s *expectMatcherStep) String() string {
	return fmt.Sprintf("Expect(%q)", s.matcher)
}

type sendStep struct {
	key Key
}

func (s *sendStep) execute(_ context.Context, _ *Console, appInput io.Writer) error {
	_, err := appInput.Write(s.key.bytes)
	if err != nil {
		return fmt.Errorf("ugh: failed to send key: %w", err)
	}
	time.Sleep(10 * time.Millisecond)
	return nil
}

func (s *sendStep) String() string {
	return fmt.Sprintf("Send(%x)", s.key.bytes)
}

type sendTextStep struct {
	text string
}

func (s *sendTextStep) execute(_ context.Context, _ *Console, appInput io.Writer) error {
	for _, r := range s.text {
		_, err := appInput.Write([]byte(string(r)))
		if err != nil {
			return fmt.Errorf("ugh: failed to send text: %w", err)
		}
		time.Sleep(10 * time.Millisecond)
	}
	return nil
}

func (s *sendTextStep) String() string {
	return fmt.Sprintf("SendText(%q)", s.text)
}

type selectByLabelStep struct {
	title   string
	label   string
	timeout time.Duration
}

func (s *selectByLabelStep) execute(ctx context.Context, c *Console, appInput io.Writer) error {
	// Allow the screen to stabilize before parsing option positions.
	// Without this, the cursor marker (">") may reflect a stale render
	// frame — e.g. after a validation rejection resets the selection.
	time.Sleep(100 * time.Millisecond)

	deadline := time.After(s.timeout)
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()

	for {
		screen := c.Screen()
		if strings.Contains(screen, s.title) && strings.Contains(screen, s.label) {
			options, cursorIdx := parseSelectOptions(screen, s.title)
			targetIdx := findOptionIndex(options, s.label)
			if targetIdx >= 0 && cursorIdx >= 0 {
				offset := targetIdx - cursorIdx
				key := Down
				if offset < 0 {
					key = Up
					offset = -offset
				}
				for i := 0; i < offset; i++ {
					if _, err := appInput.Write(key.bytes); err != nil {
						return fmt.Errorf("ugh: failed to send navigation key: %w", err)
					}
					time.Sleep(10 * time.Millisecond)
				}
				return nil
			}
		}

		select {
		case <-ctx.Done():
			return fmt.Errorf("ugh: context cancelled while waiting for option %q in select %q\n\nScreen:\n%s", s.label, s.title, c.Screen())
		case <-deadline:
			return fmt.Errorf("ugh: timeout waiting for option %q in select %q\n\nScreen:\n%s", s.label, s.title, c.Screen())
		case <-ticker.C:
		}
	}
}

func (s *selectByLabelStep) String() string {
	return fmt.Sprintf("SelectByLabel(%q, %q)", s.title, s.label)
}

type multiSelectByIndicesStep struct {
	title   string
	indices []int
}

func (s *multiSelectByIndicesStep) execute(_ context.Context, _ *Console, appInput io.Writer) error {
	time.Sleep(100 * time.Millisecond)

	currentPos := 0
	for _, idx := range s.indices {
		offset := idx - currentPos
		key := Down
		if offset < 0 {
			key = Up
			offset = -offset
		}
		for i := 0; i < offset; i++ {
			if _, err := appInput.Write(key.bytes); err != nil {
				return fmt.Errorf("ugh: failed to send navigation key: %w", err)
			}
			time.Sleep(10 * time.Millisecond)
		}
		if _, err := appInput.Write(Space.bytes); err != nil {
			return fmt.Errorf("ugh: failed to send space: %w", err)
		}
		time.Sleep(10 * time.Millisecond)
		currentPos = idx
	}
	return nil
}

func (s *multiSelectByIndicesStep) String() string {
	return fmt.Sprintf("MultiSelectByIndices(%q, %v)", s.title, s.indices)
}

type multiSelectByLabelsStep struct {
	title  string
	labels []string
}

func (s *multiSelectByLabelsStep) execute(ctx context.Context, c *Console, appInput io.Writer) error {
	time.Sleep(100 * time.Millisecond)

	for _, label := range s.labels {
		if err := s.navigateAndToggle(ctx, c, appInput, label); err != nil {
			return err
		}
	}
	return nil
}

func (s *multiSelectByLabelsStep) navigateAndToggle(ctx context.Context, c *Console, appInput io.Writer, label string) error {
	maxAttempts := 50
	for range maxAttempts {
		screen := c.Screen()
		options, cursorIdx := parseSelectOptions(screen, s.title)

		cursorLabel := findCursorOption(screen, s.title)
		if cursorLabel != "" && strings.Contains(cursorLabel, label) {
			if _, err := appInput.Write(Space.bytes); err != nil {
				return fmt.Errorf("ugh: failed to send space: %w", err)
			}
			time.Sleep(50 * time.Millisecond)
			return nil
		}

		targetIdx := findOptionIndex(options, label)
		if targetIdx >= 0 && cursorIdx >= 0 {
			offset := targetIdx - cursorIdx
			key := Down
			if offset < 0 {
				key = Up
				offset = -offset
			}
			for i := 0; i < offset; i++ {
				if _, err := appInput.Write(key.bytes); err != nil {
					return fmt.Errorf("ugh: failed to send navigation key: %w", err)
				}
				time.Sleep(10 * time.Millisecond)
			}
			time.Sleep(50 * time.Millisecond)

			if _, err := appInput.Write(Space.bytes); err != nil {
				return fmt.Errorf("ugh: failed to send space: %w", err)
			}
			time.Sleep(50 * time.Millisecond)
			return nil
		}

		if _, err := appInput.Write(Down.bytes); err != nil {
			return fmt.Errorf("ugh: failed to send Down key: %w", err)
		}
		time.Sleep(50 * time.Millisecond)

		select {
		case <-ctx.Done():
			return fmt.Errorf("ugh: context cancelled while looking for option %q", label)
		default:
		}
	}

	return fmt.Errorf("ugh: option %q not found in multiselect %q after %d attempts\n\nScreen:\n%s", label, s.title, maxAttempts, c.Screen())
}

func (s *multiSelectByLabelsStep) String() string {
	return fmt.Sprintf("MultiSelectByLabels(%q, %v)", s.title, s.labels)
}
