package player

import (
	"fmt"
	"os"
	"os/exec"
	"sync"
	"time"
)

// CrescendoConfig holds volume ramp settings.
type CrescendoConfig struct {
	Enabled   bool
	StartPct  int // 0-100
	EndPct    int // 0-100
	DurationS int // seconds to ramp
}

// Player handles alarm sound playback by shelling out to system audio tools.
type Player struct {
	mu           sync.Mutex
	stopCh       chan struct{}
	doneCh       chan struct{}
	crescStopCh  chan struct{}
	crescDoneCh  chan struct{}
}

func New() *Player {
	return &Player{}
}

// resolveSound finds the actual file path for a sound. Returns "" if no file found.
func resolveSound(path string) string {
	if path != "default" && path != "" {
		if _, err := os.Stat(path); err == nil {
			return path
		}
		return ""
	}
	candidates := []string{
		"/usr/share/sounds/freedesktop/stereo/alarm-clock-elapsed.oga",
		"/usr/share/sounds/gnome/default/alerts/bark.ogg",
		"/usr/share/sounds/sound-icons/trumpet-12.wav",
	}
	for _, c := range candidates {
		if _, err := os.Stat(c); err == nil {
			return c
		}
	}
	return ""
}

// findPlayer returns the name and arg builder for the first available audio player.
func findPlayer() (string, func(string) []string) {
	players := []struct {
		name string
		args func(string) []string
	}{
		{"paplay", func(f string) []string { return []string{f} }},
		{"aplay", func(f string) []string { return []string{f} }},
		{"mpv", func(f string) []string { return []string{"--no-video", "--no-terminal", f} }},
		{"ffplay", func(f string) []string { return []string{"-nodisp", "-autoexit", f} }},
	}
	for _, pl := range players {
		if _, err := exec.LookPath(pl.name); err == nil {
			return pl.name, pl.args
		}
	}
	return "", nil
}

// PlayLoop starts playing a sound file in a loop until Stop() is called.
func (p *Player) PlayLoop(path string) {
	p.PlayLoopWithCrescendo(path, CrescendoConfig{})
}

// PlayLoopWithCrescendo starts playing with optional volume ramp.
func (p *Player) PlayLoopWithCrescendo(path string, cresc CrescendoConfig) {
	p.Stop() // kill any previous playback and wait for it to finish

	p.mu.Lock()
	p.stopCh = make(chan struct{})
	p.doneCh = make(chan struct{})
	stopCh := p.stopCh
	doneCh := p.doneCh
	p.mu.Unlock()

	// Start crescendo if enabled and pactl is available
	if cresc.Enabled && cresc.DurationS > 0 {
		if _, err := exec.LookPath("pactl"); err == nil {
			p.mu.Lock()
			p.crescStopCh = make(chan struct{})
			p.crescDoneCh = make(chan struct{})
			crescStopCh := p.crescStopCh
			crescDoneCh := p.crescDoneCh
			p.mu.Unlock()
			go p.crescendoLoop(cresc, crescStopCh, crescDoneCh)
		}
	}

	resolved := resolveSound(path)
	if resolved == "" {
		go p.bellLoop(stopCh, doneCh)
		return
	}

	name, argsFn := findPlayer()
	if name == "" {
		os.Stdout.WriteString("\a")
		close(doneCh)
		return
	}

	go p.audioLoop(name, argsFn, resolved, stopCh, doneCh)
}

// audioLoop runs the audio player in a loop. The goroutine fully owns the
// process — no other goroutine touches cmd. Coordination is only via channels.
func (p *Player) audioLoop(name string, argsFn func(string) []string, path string, stopCh, doneCh chan struct{}) {
	defer close(doneCh)

	for {
		// Check if we should stop before starting a new process
		select {
		case <-stopCh:
			return
		default:
		}

		cmd := exec.Command(name, argsFn(path)...)
		cmd.Stdout = nil
		cmd.Stderr = nil

		if err := cmd.Start(); err != nil {
			return
		}

		// Wait for either the process to finish or a stop signal
		processDone := make(chan error, 1)
		go func() {
			processDone <- cmd.Wait()
		}()

		select {
		case <-stopCh:
			// Kill the process and wait for it to exit
			_ = cmd.Process.Kill()
			<-processDone
			return
		case <-processDone:
			// Process finished naturally, loop again after a brief pause
		}

		// Pause between loops
		select {
		case <-stopCh:
			return
		case <-time.After(300 * time.Millisecond):
		}
	}
}

func (p *Player) bellLoop(stopCh, doneCh chan struct{}) {
	defer close(doneCh)
	ticker := time.NewTicker(2 * time.Second)
	defer ticker.Stop()
	os.Stdout.WriteString("\a")
	for {
		select {
		case <-stopCh:
			return
		case <-ticker.C:
			os.Stdout.WriteString("\a")
		}
	}
}

// crescendoLoop gradually raises PulseAudio volume from StartPct to EndPct over DurationS seconds.
func (p *Player) crescendoLoop(cfg CrescendoConfig, stopCh, doneCh chan struct{}) {
	defer close(doneCh)

	startPct := cfg.StartPct
	endPct := cfg.EndPct
	if startPct < 0 {
		startPct = 0
	}
	if endPct > 150 { // allow some overdrive but not ridiculous
		endPct = 150
	}
	if startPct >= endPct {
		return
	}

	// Set initial volume
	setVolume(startPct)

	steps := cfg.DurationS
	if steps < 1 {
		steps = 1
	}
	stepSize := float64(endPct-startPct) / float64(steps)
	ticker := time.NewTicker(time.Second)
	defer ticker.Stop()

	currentPct := float64(startPct)
	for i := 0; i < steps; i++ {
		select {
		case <-stopCh:
			return
		case <-ticker.C:
			currentPct += stepSize
			setVolume(int(currentPct))
		}
	}
	// Ensure we hit the target
	setVolume(endPct)
}

// setVolume sets the default PulseAudio sink volume to the given percentage.
func setVolume(pct int) {
	_ = exec.Command("pactl", "set-sink-volume", "@DEFAULT_SINK@", fmt.Sprintf("%d%%", pct)).Run()
}

// Stop stops any currently playing sound and waits for the playback goroutine to exit.
func (p *Player) Stop() {
	p.mu.Lock()
	stopCh := p.stopCh
	doneCh := p.doneCh
	crescStopCh := p.crescStopCh
	crescDoneCh := p.crescDoneCh
	p.stopCh = nil
	p.doneCh = nil
	p.crescStopCh = nil
	p.crescDoneCh = nil
	p.mu.Unlock()

	// Stop crescendo
	if crescStopCh != nil {
		close(crescStopCh)
	}
	if crescDoneCh != nil {
		<-crescDoneCh
	}

	// Stop audio
	if stopCh != nil {
		close(stopCh)
	}
	if doneCh != nil {
		<-doneCh // wait for goroutine to actually finish
	}
}
