// SPDX-License-Identifier: GPL-3.0-or-later

// Package output provides progress bar utilities.
package output

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

// ProgressBar represents a progress bar.
type ProgressBar struct {
	mu            sync.Mutex
	writer        io.Writer
	colors        *Colors
	width         int
	isTTY         bool
	unicode       bool
	total         int64
	current       int64
	startTime     time.Time
	lastUpdate    time.Time
	message       string
	showPercent   bool
	showRate      bool
	showETA       bool
	showCurrent   bool
	rateUnit      string
	currentUnit   string
	minUpdateTime time.Duration
}

// ProgressBarOption is an option for progress bars.
type ProgressBarOption func(*ProgressBar)

// NewProgressBar creates a new progress bar.
func NewProgressBar(writer io.Writer, colors *Colors, opts ...ProgressBarOption) *ProgressBar {
	pb := &ProgressBar{
		writer:        writer,
		colors:        colors,
		width:         40,
		isTTY:         true,
		unicode:       true,
		showPercent:   true,
		showRate:      true,
		showETA:       true,
		showCurrent:   true,
		rateUnit:      "/s",
		currentUnit:   "",
		minUpdateTime: 100 * time.Millisecond,
	}

	for _, opt := range opts {
		opt(pb)
	}

	return pb
}

// WithWidth sets the progress bar width.
func WithWidth(width int) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.width = width
	}
}

// WithTTY sets whether output is a TTY.
func WithTTY(isTTY bool) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.isTTY = isTTY
	}
}

// WithUnicode sets whether unicode is supported.
func WithUnicode(unicode bool) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.unicode = unicode
	}
}

// WithRateUnit sets the rate unit suffix.
func WithRateUnit(unit string) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.rateUnit = unit
	}
}

// WithCurrentUnit sets the current value unit.
func WithCurrentUnit(unit string) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.currentUnit = unit
	}
}

// WithShowPercent enables/disables percentage display.
func WithShowPercent(show bool) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.showPercent = show
	}
}

// WithShowRate enables/disables rate display.
func WithShowRate(show bool) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.showRate = show
	}
}

// WithShowETA enables/disables ETA display.
func WithShowETA(show bool) ProgressBarOption {
	return func(pb *ProgressBar) {
		pb.showETA = show
	}
}

// Start starts the progress bar with a total value.
func (pb *ProgressBar) Start(total int64) {
	pb.mu.Lock()
	defer pb.mu.Unlock()

	pb.total = total
	pb.current = 0
	pb.startTime = time.Now()
	pb.lastUpdate = time.Time{}
}

// SetMessage sets the progress message.
func (pb *ProgressBar) SetMessage(msg string) {
	pb.mu.Lock()
	defer pb.mu.Unlock()
	pb.message = msg
}

// Update updates the current progress.
func (pb *ProgressBar) Update(current int64) {
	pb.mu.Lock()
	defer pb.mu.Unlock()

	pb.current = current

	// Rate limit updates
	now := time.Now()
	if !pb.lastUpdate.IsZero() && now.Sub(pb.lastUpdate) < pb.minUpdateTime {
		return
	}
	pb.lastUpdate = now

	pb.render()
}

// Increment increments the progress by 1.
func (pb *ProgressBar) Increment() {
	pb.mu.Lock()
	currentCopy := pb.current + 1
	pb.mu.Unlock()
	pb.Update(currentCopy)
}

// Add adds a value to the current progress.
func (pb *ProgressBar) Add(n int64) {
	pb.mu.Lock()
	currentCopy := pb.current + n
	pb.mu.Unlock()
	pb.Update(currentCopy)
}

// Finish finishes the progress bar.
func (pb *ProgressBar) Finish() {
	pb.mu.Lock()
	defer pb.mu.Unlock()

	pb.current = pb.total
	pb.render()
	fmt.Fprintln(pb.writer)
}

// render renders the progress bar.
func (pb *ProgressBar) render() {
	if !pb.isTTY {
		// Non-TTY: simple percentage output
		pct := float64(0)
		if pb.total > 0 {
			pct = float64(pb.current) / float64(pb.total) * 100
		}
		fmt.Fprintf(pb.writer, "Progress: %.0f%% (%d/%d)\n", pct, pb.current, pb.total)
		return
	}

	// Clear line and return to start
	fmt.Fprint(pb.writer, "\r\033[K")

	var parts []string

	// Message
	if pb.message != "" {
		parts = append(parts, pb.message)
	}

	// Progress bar
	bar := pb.renderBar()
	parts = append(parts, bar)

	// Percentage
	if pb.showPercent {
		pct := float64(0)
		if pb.total > 0 {
			pct = float64(pb.current) / float64(pb.total) * 100
		}
		parts = append(parts, fmt.Sprintf("%.0f%%", pct))
	}

	// Current/Total
	if pb.showCurrent {
		parts = append(parts, fmt.Sprintf("%s/%s", formatSize(pb.current), formatSize(pb.total)))
	}

	// Rate
	if pb.showRate {
		elapsed := time.Since(pb.startTime)
		if elapsed > 0 {
			rate := float64(pb.current) / elapsed.Seconds()
			parts = append(parts, fmt.Sprintf("%s%s", formatSize(int64(rate)), pb.rateUnit))
		}
	}

	// ETA
	if pb.showETA && pb.current > 0 {
		elapsed := time.Since(pb.startTime)
		remaining := float64(pb.total-pb.current) / float64(pb.current) * float64(elapsed)
		if remaining > 0 && remaining < 24*time.Hour.Seconds()*1e9 {
			parts = append(parts, fmt.Sprintf("ETA: %s", formatDuration(time.Duration(remaining))))
		}
	}

	output := strings.Join(parts, "  ")
	fmt.Fprint(pb.writer, output)
}

// renderBar renders the bar portion.
func (pb *ProgressBar) renderBar() string {
	pct := float64(0)
	if pb.total > 0 {
		pct = float64(pb.current) / float64(pb.total)
	}
	if pct > 1 {
		pct = 1
	}

	filled := int(float64(pb.width) * pct)
	empty := pb.width - filled

	var bar string
	if pb.unicode {
		bar = pb.colors.Cyan(strings.Repeat("━", filled)) +
			pb.colors.Dim(strings.Repeat("━", empty))
	} else {
		bar = "[" + strings.Repeat("=", filled) +
			strings.Repeat(" ", empty) + "]"
	}

	return bar
}

// formatSize formats a byte size.
func formatSize(bytes int64) string {
	const (
		KB = 1024
		MB = KB * 1024
		GB = MB * 1024
		TB = GB * 1024
	)

	switch {
	case bytes >= TB:
		return fmt.Sprintf("%.1f TB", float64(bytes)/TB)
	case bytes >= GB:
		return fmt.Sprintf("%.1f GB", float64(bytes)/GB)
	case bytes >= MB:
		return fmt.Sprintf("%.1f MB", float64(bytes)/MB)
	case bytes >= KB:
		return fmt.Sprintf("%.1f KB", float64(bytes)/KB)
	default:
		return fmt.Sprintf("%d B", bytes)
	}
}

// formatDuration formats a duration for display.
func formatDuration(d time.Duration) string {
	if d < time.Minute {
		return fmt.Sprintf("%ds", int(d.Seconds()))
	}
	if d < time.Hour {
		m := int(d.Minutes())
		s := int(d.Seconds()) % 60
		if s > 0 {
			return fmt.Sprintf("%dm %ds", m, s)
		}
		return fmt.Sprintf("%dm", m)
	}
	h := int(d.Hours())
	m := int(d.Minutes()) % 60
	return fmt.Sprintf("%dh %dm", h, m)
}

// Spinner represents a spinner animation.
type Spinner struct {
	mu       sync.Mutex
	writer   io.Writer
	colors   *Colors
	message  string
	frames   []string
	current  int
	running  bool
	done     chan struct{}
	interval time.Duration
}

// SpinnerOption is an option for spinners.
type SpinnerOption func(*Spinner)

// NewSpinner creates a new spinner.
func NewSpinner(writer io.Writer, colors *Colors, opts ...SpinnerOption) *Spinner {
	s := &Spinner{
		writer:   writer,
		colors:   colors,
		frames:   []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"},
		interval: 100 * time.Millisecond,
		done:     make(chan struct{}),
	}

	for _, opt := range opts {
		opt(s)
	}

	return s
}

// WithASCIIFrames uses ASCII spinner frames.
func WithASCIIFrames() SpinnerOption {
	return func(s *Spinner) {
		s.frames = []string{"|", "/", "-", "\\"}
	}
}

// WithSpinnerInterval sets the spinner interval.
func WithSpinnerInterval(d time.Duration) SpinnerOption {
	return func(s *Spinner) {
		s.interval = d
	}
}

// SetMessage sets the spinner message.
func (s *Spinner) SetMessage(msg string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.message = msg
}

// Start starts the spinner.
func (s *Spinner) Start(message string) {
	s.mu.Lock()
	s.message = message
	s.running = true
	s.mu.Unlock()

	go s.run()
}

// Stop stops the spinner.
func (s *Spinner) Stop() {
	s.mu.Lock()
	if !s.running {
		s.mu.Unlock()
		return
	}
	s.running = false
	s.mu.Unlock()

	s.done <- struct{}{}

	// Clear line
	fmt.Fprint(s.writer, "\r\033[K")
}

// Success stops the spinner with a success message.
func (s *Spinner) Success(message string) {
	s.Stop()
	fmt.Fprintf(s.writer, "%s %s\n", s.colors.Success("✓"), message)
}

// Fail stops the spinner with a failure message.
func (s *Spinner) Fail(message string) {
	s.Stop()
	fmt.Fprintf(s.writer, "%s %s\n", s.colors.Error("✗"), message)
}

// run runs the spinner animation.
func (s *Spinner) run() {
	ticker := time.NewTicker(s.interval)
	defer ticker.Stop()

	for {
		select {
		case <-s.done:
			return
		case <-ticker.C:
			s.mu.Lock()
			if !s.running {
				s.mu.Unlock()
				return
			}
			frame := s.frames[s.current%len(s.frames)]
			message := s.message
			s.current++
			s.mu.Unlock()

			fmt.Fprintf(s.writer, "\r\033[K%s %s", s.colors.Cyan(frame), message)
		}
	}
}

// FormatSize formats a byte size for display.
func FormatSize(bytes int64) string {
	return formatSize(bytes)
}

// FormatDuration formats a duration for display.
func FormatDuration(d time.Duration) string {
	return formatDuration(d)
}
