package player

import (
	"bufio"
	"encoding/json"
	"fmt"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"sync"
	"time"
)

// Player abstracts execution of a media player for a stream URL.
// Implementations may use exec (legacy) or IPC (preferred for mpv).
type Player interface {
	// Play starts playback of the given URL. For long-running players this blocks
	// until the player exits (or returns immediately for background/IPC impls).
	Play(url string, extraArgs ...string) error
	Stop() error // best effort

	// Metadata returns the latest stream/song title observed (e.g. from icy or media-title).
	// Returns "" if unknown or not supported by the impl.
	Metadata() string

	// Control methods. Best-effort; return nil if unsupported.
	Pause() error
	Resume() error
	Mute() error
	Unmute() error
	Next() error // e.g. playlist-next or station skip; may be no-op for single stream
	Prev() error

	// Volume controls (relative, best effort; mpv uses "add volume ±5" etc.)
	VolumeUp() error
	VolumeDown() error

	// Volume returns current volume level (0-100).
	Volume() int
}

// IsInstalled reports whether the named executable is on PATH.
// Fixed to avoid shell injection (no "/bin/sh -c 'command -v ' + name").
func IsInstalled(name string) bool {
	if name == "" {
		return false
	}
	// Direct lookpath is best; fall back to exec the name with a harmless arg.
	if _, err := exec.LookPath(name); err == nil {
		return true
	}
	// Some players may be scripts without direct lookpath in certain envs; try exec --version like.
	cmd := exec.Command(name, "--version")
	_ = cmd.Run() // ignore output/err; if it started at all, likely present
	// More reliable: use LookPath on common variants? For now LookPath is primary.
	return false
}

// NewLegacy returns a simple exec-based player (stdio inheritance for interactive controls).
// This is the cleaned version of the old subExecute pattern.
func NewLegacy(program string, baseArgs ...string) Player {
	return &legacyPlayer{program: program, baseArgs: baseArgs}
}

type legacyPlayer struct {
	program  string
	baseArgs []string
	cmd      *exec.Cmd
}

func (p *legacyPlayer) Play(url string, extra ...string) error {
	args := append([]string{}, p.baseArgs...)
	args = append(args, extra...)
	args = append(args, url)

	p.cmd = exec.Command(p.program, args...)
	p.cmd.Stdin = os.Stdin
	p.cmd.Stdout = os.Stdout
	p.cmd.Stderr = os.Stderr

	if err := p.cmd.Run(); err != nil {
		// For interactive players the err is often just "exit status N" from 'q'.
		// Surface it but don't treat as fatal for the app.
		fmt.Printf("(player exited: %v)\n", err)
	}
	return nil // we intentionally do not try CombinedOutput after Run
}

func (p *legacyPlayer) Stop() error {
	if p.cmd != nil && p.cmd.Process != nil {
		_ = p.cmd.Process.Kill()
	}
	return nil
}

func (p *legacyPlayer) Metadata() string { return "" }
func (p *legacyPlayer) Pause() error     { return nil }
func (p *legacyPlayer) Resume() error    { return nil }
func (p *legacyPlayer) Mute() error      { return nil }
func (p *legacyPlayer) Unmute() error    { return nil }
func (p *legacyPlayer) Next() error      { return nil }
func (p *legacyPlayer) Prev() error      { return nil }
func (p *legacyPlayer) VolumeUp() error   { return nil }
func (p *legacyPlayer) VolumeDown() error { return nil }
func (p *legacyPlayer) Volume() int        { return 70 }

// isShellMeta reports if name looks like it contains shell metachars (defense in depth).
func isShellMeta(name string) bool {
	return strings.ContainsAny(name, ";|&`$(){}[]<>\n\r\t ")
}

// isInstalledLegacy is the old body (kept only for reference / windows where.exe).
// New code should use the top-level IsInstalled.
func isInstalledLegacy(name string) bool {
	if runtime.GOOS == "windows" {
		cmd := exec.Command("where.exe", name)
		return cmd.Run() == nil
	}
	if isShellMeta(name) {
		return false // refuse obviously bad names
	}
	cmd := exec.Command("/bin/sh", "-c", "command -v "+name) // still used on some old paths; prefer LookPath above
	return cmd.Run() == nil
}

// NewMpv returns an mpv-based player using JSON IPC over a unix socket for
// non-blocking background playback, control (pause/mute/etc), and streamed
// metadata observation (media-title / metadata). Falls back gracefully to
// audio-only if IPC socket cannot be connected (still starts the mpv process).
func NewMpv(program string, baseArgs ...string) Player {
	return &mpvPlayer{
		program:  program,
		baseArgs: baseArgs,
	}
}

type mpvPlayer struct {
	program  string
	baseArgs []string

	socket string
	cmd    *exec.Cmd
	conn   net.Conn

	mu     sync.Mutex
	title  string
	paused bool
	muted  bool
	vol    int
}

func (p *mpvPlayer) Play(url string, extra ...string) error {
	// unique socket per run
	p.socket = filepath.Join(os.TempDir(), fmt.Sprintf("gostations-mpv-%d-%d.sock", os.Getpid(), time.Now().UnixNano()))
	_ = os.Remove(p.socket)

	args := append([]string{}, p.baseArgs...)
	args = append(args, extra...)
	args = append(args,
		"--no-terminal",
		"--really-quiet",
		"--vo=null",
		"--input-ipc-server="+p.socket,
		"--idle=no",
		url,
	)

	p.cmd = exec.Command(p.program, args...)
	// detached: no stdio attach so TUI remains responsive
	if err := p.cmd.Start(); err != nil {
		return err
	}

	// give mpv a moment to create the socket
	time.Sleep(150 * time.Millisecond)
	for i := 0; i < 30; i++ {
		if _, err := os.Stat(p.socket); err == nil {
			break
		}
		time.Sleep(30 * time.Millisecond)
	}

	conn, err := net.DialTimeout("unix", p.socket, 500*time.Millisecond)
	if err != nil {
		// audio may still be playing; controls/metadata unavailable
		return nil
	}
	p.conn = conn

	// observe properties we care about
	p.send(map[string]any{"command": []any{"observe_property", 1, "media-title"}})
	p.send(map[string]any{"command": []any{"observe_property", 2, "metadata"}})
	p.send(map[string]any{"command": []any{"observe_property", 3, "pause"}})
	p.send(map[string]any{"command": []any{"observe_property", 4, "mute"}})
	p.send(map[string]any{"command": []any{"observe_property", 5, "volume"}})
	// get initial volume
	p.send(map[string]any{"command": []any{"get_property", "volume"}})

	go p.readLoop()
	return nil
}

func (p *mpvPlayer) send(cmd map[string]any) {
	if p.conn == nil {
		return
	}
	b, _ := json.Marshal(cmd)
	_, _ = p.conn.Write(append(b, '\n'))
}

func (p *mpvPlayer) readLoop() {
	sc := bufio.NewScanner(p.conn)
	for sc.Scan() {
		var msg map[string]any
		if json.Unmarshal(sc.Bytes(), &msg) != nil {
			continue
		}
		if msg["event"] == "property-change" {
			id, _ := msg["id"].(float64)
			data := msg["data"]
			p.mu.Lock()
			switch id {
			case 1:
				if s, ok := data.(string); ok {
					p.title = s
				}
			case 2:
				if m, ok := data.(map[string]any); ok {
					if t, ok := m["title"].(string); ok && t != "" {
						p.title = t
					} else if t, ok := m["icy-title"].(string); ok && t != "" {
						p.title = t
					}
				}
			case 3:
				if b, ok := data.(bool); ok {
					p.paused = b
				}
			case 4:
				if b, ok := data.(bool); ok {
					p.muted = b
				}
			case 5:
				if f, ok := data.(float64); ok {
					p.vol = int(f + 0.5)
				}
			}
			p.mu.Unlock()
			continue
		}
		// handle get_property response for initial volume
		if data, ok := msg["data"]; ok {
			if f, ok := data.(float64); ok {
				if req, _ := msg["request_id"].(float64); req == 0 { // rough
					p.mu.Lock()
					p.vol = int(f + 0.5)
					p.mu.Unlock()
				}
			}
		}
	}
}

func (p *mpvPlayer) Metadata() string {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.title
}

func (p *mpvPlayer) Pause() error {
	p.mu.Lock()
	p.paused = true
	p.mu.Unlock()
	p.send(map[string]any{"command": []any{"set_property", "pause", true}})
	return nil
}

func (p *mpvPlayer) Resume() error {
	p.mu.Lock()
	p.paused = false
	p.mu.Unlock()
	p.send(map[string]any{"command": []any{"set_property", "pause", false}})
	return nil
}

func (p *mpvPlayer) Mute() error {
	p.mu.Lock()
	p.muted = true
	p.mu.Unlock()
	p.send(map[string]any{"command": []any{"set_property", "mute", true}})
	return nil
}

func (p *mpvPlayer) Unmute() error {
	p.mu.Lock()
	p.muted = false
	p.mu.Unlock()
	p.send(map[string]any{"command": []any{"set_property", "mute", false}})
	return nil
}

func (p *mpvPlayer) Next() error {
	p.send(map[string]any{"command": []any{"playlist-next"}})
	return nil
}

func (p *mpvPlayer) Prev() error {
	p.send(map[string]any{"command": []any{"playlist-prev"}})
	return nil
}

func (p *mpvPlayer) VolumeUp() error {
	p.send(map[string]any{"command": []any{"add", "volume", 5}})
	return nil
}

func (p *mpvPlayer) VolumeDown() error {
	p.send(map[string]any{"command": []any{"add", "volume", -5}})
	return nil
}

func (p *mpvPlayer) Volume() int {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.vol
}

func (p *mpvPlayer) Stop() error {
	if p.conn != nil {
		p.send(map[string]any{"command": []any{"quit"}})
		_ = p.conn.Close()
		p.conn = nil
	}
	if p.cmd != nil && p.cmd.Process != nil {
		_ = p.cmd.Process.Kill()
		p.cmd = nil
	}
	_ = os.Remove(p.socket)
	p.mu.Lock()
	p.title = ""
	p.paused = false
	p.muted = false
	p.mu.Unlock()
	return nil
}
