// Package client provides a comprehensive HTTP client for controlling Bose SoundTouch devices.
//
// This package implements the complete Bose SoundTouch Web API, enabling full programmatic
// control of SoundTouch speakers including playback control, volume management, source
// selection, multiroom zone management, and real-time event monitoring.
//
// # Basic Usage
//
// Create a client and control your SoundTouch device:
//
//	config := &client.Config{
//		Host: "192.0.2.100",
//		Port: 8090,
//		Timeout: 10 * time.Second,
//	}
//	client := client.NewClient(config)
//
//	// Get device information
//	info, err := client.GetInfo()
//	if err != nil {
//		log.Fatal(err)
//	}
//	fmt.Printf("Device: %s (Type: %s)\n", info.Name, info.Type)
//
//	// Control playback
//	err = client.Play()
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	// Adjust volume
//	err = client.SetVolume(50)
//	if err != nil {
//		log.Fatal(err)
//	}
//
// # Advanced Features
//
// The client supports all SoundTouch API endpoints:
//
//	// Get current playback status
//	nowPlaying, err := client.GetNowPlaying()
//	if err != nil {
//		log.Fatal(err)
//	}
//	fmt.Printf("Now Playing: %s by %s\n", nowPlaying.Track, nowPlaying.Artist)
//
//	// Select audio source
//	err = client.SelectSource("SPOTIFY", "")
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	// Control bass and balance
//	err = client.SetBass(3)  // Range: -9 to +9
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	err = client.SetBalance(-10)  // Range: -50 (left) to +50 (right)
//	if err != nil {
//		log.Fatal(err)
//	}
//
// # Multiroom Zone Management
//
// Create and manage multiroom zones:
//
//	// Get current zone configuration
//	zone, err := client.GetZone()
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	// Create a new zone with multiple speakers
//	newZone := &models.ZoneRequest{
//		Master: "192.0.2.100",
//		Members: []models.MemberEntry{
//			{IP: "192.0.2.101"},
//			{IP: "192.0.2.102"},
//		},
//	}
//	err = client.SetZone(newZone)
//	if err != nil {
//		log.Fatal(err)
//	}
//
// # Real-time Events
//
// Monitor device state changes using WebSocket connections:
//
//	ctx := context.Background()
//	events, err := client.SubscribeToEvents(ctx)
//	if err != nil {
//		log.Fatal(err)
//	}
//
//	for event := range events {
//		switch e := event.(type) {
//		case *models.NowPlayingUpdated:
//			fmt.Printf("Track changed: %s\n", e.Track)
//		case *models.VolumeUpdated:
//			fmt.Printf("Volume: %d\n", e.ActualVolume)
//		case *models.ConnectionStateUpdated:
//			fmt.Printf("Connection: %s\n", e.State)
//		}
//	}
//
// # Error Handling
//
// The client provides detailed error information:
//
//	err := client.SetVolume(150)  // Invalid volume
//	if err != nil {
//		fmt.Printf("Error: %v\n", err)  // Will indicate volume out of range
//	}
//
// # Configuration
//
// The Config struct supports various options:
//
//	config := &client.Config{
//		Host:      "192.0.2.100",
//		Port:      8090,
//		Timeout:   15 * time.Second,
//		UserAgent: "MyApp/1.0",
//	}
//
// # Supported Operations
//
//   - Device Information & Capabilities
//   - Playback Control (Play/Pause/Stop/Next/Previous/Key commands)
//   - Volume Control (Get/Set/Increment/Decrement)
//   - Bass Control (-9 to +9 range)
//   - Balance Control (-50 to +50 range)
//   - Source Selection (Spotify, Bluetooth, AUX, Radio, etc.)
//   - Preset Management (Get configured presets)
//   - Clock/Time Management
//   - Network Information
//   - Multiroom Zone Management
//   - Real-time WebSocket Event Monitoring
package client

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/gesellix/bose-soundtouch/pkg/models"
	"github.com/gesellix/bose-soundtouch/pkg/speaker"
)

// Client represents a SoundTouch API client
type Client struct {
	baseURL    string
	httpClient *http.Client
	timeout    time.Duration
	userAgent  string

	// avTransportURLOverride, when set, replaces the UPnP AVTransport control
	// URL that is otherwise derived from baseURL (host + speaker.UPnPPort). Used
	// only by tests to point the SOAP requests at an httptest server.
	avTransportURLOverride string
}

// Config holds configuration for the SoundTouch client
type Config struct {
	Host      string
	Port      int
	Timeout   time.Duration
	UserAgent string
}

// DefaultConfig returns a default client configuration
func DefaultConfig() *Config {
	return &Config{
		Host:      "localhost",
		Port:      8090,
		Timeout:   30 * time.Second,
		UserAgent: "Bose-SoundTouch-Go-Client/1.0",
	}
}

// NewClient creates a new SoundTouch API client
func NewClient(config *Config) *Client {
	if config.Timeout == 0 {
		config.Timeout = 10 * time.Second
	}

	if config.UserAgent == "" {
		config.UserAgent = "Bose-SoundTouch-Go-Client/1.0"
	}

	host := config.Host
	if !strings.Contains(host, "://") {
		host = "http://" + host
	}

	u, err := url.Parse(host)
	if err != nil {
		// Fallback for invalid URLs
		port := config.Port
		if port == 0 {
			port = speaker.HTTPPort
		}

		return &Client{
			baseURL: fmt.Sprintf("http://%s:%d", config.Host, port),
			httpClient: &http.Client{
				Timeout: config.Timeout,
			},
			timeout:   config.Timeout,
			userAgent: config.UserAgent,
		}
	}

	// Use SplitHostPort to check for port in the host string
	_, p, splitErr := net.SplitHostPort(u.Host)
	if splitErr != nil {
		// No port in the host string, use the one from config or default
		port := config.Port
		if port == 0 {
			port = speaker.HTTPPort
		}

		u.Host = net.JoinHostPort(u.Host, fmt.Sprintf("%d", port))
	} else if p == "" {
		// Empty port, use config or default
		port := config.Port
		if port == 0 {
			port = speaker.HTTPPort
		}

		u.Host = net.JoinHostPort(u.Hostname(), fmt.Sprintf("%d", port))
	}

	return &Client{
		baseURL: u.String(),
		httpClient: &http.Client{
			Timeout: config.Timeout,
		},
		timeout:   config.Timeout,
		userAgent: config.UserAgent,
	}
}

// NewClientFromHost creates a new client with just a host address
func NewClientFromHost(host string) *Client {
	config := DefaultConfig()
	config.Host = host

	return NewClient(config)
}

// GetDeviceInfo retrieves device information from the /info endpoint
func (c *Client) GetDeviceInfo() (*models.DeviceInfo, error) {
	var deviceInfo models.DeviceInfo

	err := c.get("/info", &deviceInfo)
	if err != nil {
		return nil, fmt.Errorf("failed to get device info: %w", err)
	}

	return &deviceInfo, nil
}

// GetNowPlaying retrieves current playback information from the /now_playing endpoint
func (c *Client) GetNowPlaying() (*models.NowPlaying, error) {
	var nowPlaying models.NowPlaying

	err := c.get("/now_playing", &nowPlaying)
	if err != nil {
		return nil, fmt.Errorf("failed to get now playing: %w", err)
	}

	return &nowPlaying, nil
}

// GetSources retrieves available audio sources from the /sources endpoint
func (c *Client) GetSources() (*models.Sources, error) {
	var sources models.Sources

	err := c.get("/sources", &sources)
	if err != nil {
		return nil, fmt.Errorf("failed to get sources: %w", err)
	}

	return &sources, nil
}

// GetServiceAvailability retrieves service availability status from the /serviceAvailability endpoint
func (c *Client) GetServiceAvailability() (*models.ServiceAvailability, error) {
	var serviceAvailability models.ServiceAvailability

	err := c.get("/serviceAvailability", &serviceAvailability)
	if err != nil {
		return nil, fmt.Errorf("failed to get service availability: %w", err)
	}

	return &serviceAvailability, nil
}

// ListMediaServers returns the DLNA media servers that the speaker itself has
// discovered on the LAN (via its own UPnP sweep). The response may be empty
// when the speaker has not yet discovered any servers; that is not an error.
func (c *Client) ListMediaServers() (*models.ListMediaServersResponse, error) {
	var resp models.ListMediaServersResponse

	if err := c.get("/listMediaServers", &resp); err != nil {
		return nil, fmt.Errorf("failed to list media servers: %w", err)
	}

	return &resp, nil
}

// GetName retrieves the device name from the /name endpoint
func (c *Client) GetName() (*models.Name, error) {
	var name models.Name

	err := c.get("/name", &name)
	if err != nil {
		return nil, fmt.Errorf("failed to get device name: %w", err)
	}

	return &name, nil
}

// GetCapabilities retrieves device capabilities from the /capabilities endpoint
func (c *Client) GetCapabilities() (*models.Capabilities, error) {
	var capabilities models.Capabilities

	err := c.get("/capabilities", &capabilities)
	if err != nil {
		return nil, fmt.Errorf("failed to get device capabilities: %w", err)
	}

	return &capabilities, nil
}

// GetSupportedURLs retrieves all supported endpoints from the /supportedURLs endpoint
func (c *Client) GetSupportedURLs() (*models.SupportedURLsResponse, error) {
	var supportedURLs models.SupportedURLsResponse

	err := c.get("/supportedURLs", &supportedURLs)
	if err != nil {
		return nil, fmt.Errorf("failed to get supported URLs: %w", err)
	}

	return &supportedURLs, nil
}

// GetPresets retrieves configured presets from the /presets endpoint
func (c *Client) GetPresets() (*models.Presets, error) {
	var presets models.Presets

	err := c.get("/presets", &presets)
	if err != nil {
		return nil, fmt.Errorf("failed to get presets: %w", err)
	}

	return &presets, nil
}

// GetNextAvailablePresetSlot returns the next available preset slot (1-6), or error if all are used
func (c *Client) GetNextAvailablePresetSlot() (int, error) {
	presets, err := c.GetPresets()
	if err != nil {
		return 0, fmt.Errorf("failed to get presets: %w", err)
	}

	emptySlots := presets.GetEmptyPresetSlots()
	if len(emptySlots) == 0 {
		return 0, fmt.Errorf("all preset slots are occupied")
	}

	// Return the first available slot
	return emptySlots[0], nil
}

// IsCurrentContentPresetable checks if the currently playing content can be saved as a preset
func (c *Client) IsCurrentContentPresetable() (bool, error) {
	nowPlaying, err := c.GetNowPlaying()
	if err != nil {
		return false, fmt.Errorf("failed to get now playing: %w", err)
	}

	if nowPlaying.IsEmpty() || nowPlaying.ContentItem == nil {
		return false, nil
	}

	return nowPlaying.ContentItem.IsPresetable, nil
}

// StorePreset saves content as a preset on the SoundTouch device
func (c *Client) StorePreset(id int, contentItem *models.ContentItem) error {
	if id < 1 || id > 6 {
		return fmt.Errorf("preset ID must be between 1 and 6, got %d", id)
	}

	if contentItem == nil {
		return fmt.Errorf("content item cannot be nil")
	}

	now := time.Now().Unix()
	preset := &models.Preset{
		ID:          id,
		CreatedOn:   &now,
		UpdatedOn:   &now,
		ContentItem: contentItem,
	}

	err := c.post("/storePreset", preset)
	if err != nil {
		return fmt.Errorf("failed to store preset %d: %w", id, err)
	}

	return nil
}

// StoreCurrentAsPreset saves currently playing content as preset
func (c *Client) StoreCurrentAsPreset(id int) error {
	if id < 1 || id > 6 {
		return fmt.Errorf("preset ID must be between 1 and 6, got %d", id)
	}

	nowPlaying, err := c.GetNowPlaying()
	if err != nil {
		return fmt.Errorf("failed to get current content: %w", err)
	}

	if nowPlaying.IsEmpty() || nowPlaying.ContentItem == nil {
		return fmt.Errorf("no content currently playing")
	}

	if !nowPlaying.ContentItem.IsPresetable {
		return fmt.Errorf("current content cannot be saved as preset")
	}

	// Streaming services (Spotify, TuneIn, …) put the artwork URL in the
	// top-level <art> element of the now-playing response, not inside
	// ContentItem.containerArt.  Copy it over before storing so the preset
	// slot shows the album/station cover image.
	ci := *nowPlaying.ContentItem // shallow copy — no pointer fields in ContentItem
	if ci.ContainerArt == "" &&
		nowPlaying.Art != nil &&
		nowPlaying.Art.ArtImageStatus == "IMAGE_PRESENT" &&
		nowPlaying.Art.URL != "" {
		ci.ContainerArt = nowPlaying.Art.URL
	}

	return c.StorePreset(id, &ci)
}

// RemovePreset deletes a preset from the SoundTouch device
func (c *Client) RemovePreset(id int) error {
	if id < 1 || id > 6 {
		return fmt.Errorf("preset ID must be between 1 and 6, got %d", id)
	}

	preset := &models.Preset{ID: id}

	err := c.post("/removePreset", preset)
	if err != nil {
		return fmt.Errorf("failed to remove preset %d: %w", id, err)
	}

	return nil
}

// SendKey sends a key press command to the device (press followed by release)
func (c *Client) SendKey(keyValue string) error {
	if !models.IsValidKey(keyValue) {
		return fmt.Errorf("invalid key value: %s", keyValue)
	}

	// Send press state
	keyPress := models.NewKey(keyValue)

	err := c.post("/key", keyPress)
	if err != nil {
		return fmt.Errorf("failed to send key press: %w", err)
	}

	// Send release state
	keyRelease := models.NewKeyRelease(keyValue)

	err = c.post("/key", keyRelease)
	if err != nil {
		return fmt.Errorf("failed to send key release: %w", err)
	}

	return nil
}

// SendKeyPress sends a key press command (alias for SendKey - sends press+release)
func (c *Client) SendKeyPress(keyValue string) error {
	return c.SendKey(keyValue)
}

// SendKeyPressOnly sends only the key press state (without release)
func (c *Client) SendKeyPressOnly(keyValue string) error {
	if !models.IsValidKey(keyValue) {
		return fmt.Errorf("invalid key value: %s", keyValue)
	}

	key := models.NewKey(keyValue)

	return c.post("/key", key)
}

// SendKeyRelease sends a key release command
func (c *Client) SendKeyRelease(keyValue string) error {
	if !models.IsValidKey(keyValue) {
		return fmt.Errorf("invalid key value: %s", keyValue)
	}

	key := models.NewKeyRelease(keyValue)

	return c.post("/key", key)
}

// SendKeyReleaseOnly sends only the key release state (alias for SendKeyRelease)
func (c *Client) SendKeyReleaseOnly(keyValue string) error {
	return c.SendKeyRelease(keyValue)
}

// Play sends a PLAY key command
func (c *Client) Play() error {
	return c.SendKey(models.KeyPlay)
}

// Pause sends a PAUSE key command
func (c *Client) Pause() error {
	return c.SendKey(models.KeyPause)
}

// Stop sends a STOP key command
func (c *Client) Stop() error {
	return c.SendKey(models.KeyStop)
}

// NextTrack sends a NEXT_TRACK key command
func (c *Client) NextTrack() error {
	return c.SendKey(models.KeyNextTrack)
}

// PrevTrack sends a PREV_TRACK key command
func (c *Client) PrevTrack() error {
	return c.SendKey(models.KeyPrevTrack)
}

// VolumeUp sends a VOLUME_UP key command
func (c *Client) VolumeUp() error {
	return c.SendKey(models.KeyVolumeUp)
}

// VolumeDown sends a VOLUME_DOWN key command
func (c *Client) VolumeDown() error {
	return c.SendKey(models.KeyVolumeDown)
}

// SelectPreset sends a preset key command (1-6)
func (c *Client) SelectPreset(presetNumber int) error {
	var keyValue string

	switch presetNumber {
	case 1:
		keyValue = models.KeyPreset1
	case 2:
		keyValue = models.KeyPreset2
	case 3:
		keyValue = models.KeyPreset3
	case 4:
		keyValue = models.KeyPreset4
	case 5:
		keyValue = models.KeyPreset5
	case 6:
		keyValue = models.KeyPreset6
	default:
		return fmt.Errorf("invalid preset number: %d (must be 1-6)", presetNumber)
	}

	return c.SendKey(keyValue)
}

// GetVolume retrieves the current volume level from the /volume endpoint
func (c *Client) GetVolume() (*models.Volume, error) {
	var volume models.Volume

	err := c.get("/volume", &volume)
	if err != nil {
		return nil, fmt.Errorf("failed to get volume: %w", err)
	}

	return &volume, nil
}

// SetVolume sets the volume level using the /volume endpoint
func (c *Client) SetVolume(level int) error {
	if !models.ValidateVolumeLevel(level) {
		return fmt.Errorf("invalid volume level: %d (must be 0-100)", level)
	}

	volumeReq := models.NewVolumeRequest(level)

	return c.post("/volume", volumeReq)
}

// SetVolumeSafe sets volume with validation and clamping
func (c *Client) SetVolumeSafe(level int) error {
	clampedLevel := models.ClampVolumeLevel(level)
	return c.SetVolume(clampedLevel)
}

// IncreaseVolume increases volume by the specified amount (with safety limits)
func (c *Client) IncreaseVolume(amount int) (*models.Volume, error) {
	currentVolume, err := c.GetVolume()
	if err != nil {
		return nil, fmt.Errorf("failed to get current volume: %w", err)
	}

	newLevel := models.ClampVolumeLevel(currentVolume.GetLevel() + amount)

	err = c.SetVolume(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set volume: %w", err)
	}

	// Return updated volume
	return c.GetVolume()
}

// DecreaseVolume decreases volume by the specified amount (with safety limits)
func (c *Client) DecreaseVolume(amount int) (*models.Volume, error) {
	currentVolume, err := c.GetVolume()
	if err != nil {
		return nil, fmt.Errorf("failed to get current volume: %w", err)
	}

	newLevel := models.ClampVolumeLevel(currentVolume.GetLevel() - amount)

	err = c.SetVolume(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set volume: %w", err)
	}

	// Return updated volume
	return c.GetVolume()
}

// GetBass retrieves the current bass level from the /bass endpoint
func (c *Client) GetBass() (*models.Bass, error) {
	var bass models.Bass

	err := c.get("/bass", &bass)
	if err != nil {
		return nil, fmt.Errorf("failed to get bass: %w", err)
	}

	return &bass, nil
}

// SetBass sets the bass level using the /bass endpoint
func (c *Client) SetBass(level int) error {
	if !models.ValidateBassLevel(level) {
		return fmt.Errorf("invalid bass level: %d (must be between %d and %d)", level, models.BassLevelMin, models.BassLevelMax)
	}

	bassReq, err := models.NewBassRequest(level)
	if err != nil {
		return fmt.Errorf("failed to create bass request: %w", err)
	}

	return c.post("/bass", bassReq)
}

// SetBassSafe sets bass with validation and clamping
func (c *Client) SetBassSafe(level int) error {
	clampedLevel := models.ClampBassLevel(level)
	return c.SetBass(clampedLevel)
}

// IncreaseBass increases bass by the specified amount (with safety limits)
func (c *Client) IncreaseBass(amount int) (*models.Bass, error) {
	currentBass, err := c.GetBass()
	if err != nil {
		return nil, fmt.Errorf("failed to get current bass: %w", err)
	}

	newLevel := models.ClampBassLevel(currentBass.GetLevel() + amount)

	err = c.SetBass(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set bass: %w", err)
	}

	// Return updated bass
	return c.GetBass()
}

// DecreaseBass decreases bass by the specified amount (with safety limits)
func (c *Client) DecreaseBass(amount int) (*models.Bass, error) {
	currentBass, err := c.GetBass()
	if err != nil {
		return nil, fmt.Errorf("failed to get current bass: %w", err)
	}

	newLevel := models.ClampBassLevel(currentBass.GetLevel() - amount)

	err = c.SetBass(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set bass: %w", err)
	}

	// Return updated bass
	return c.GetBass()
}

// GetBalance retrieves the current balance level from the /balance endpoint
func (c *Client) GetBalance() (*models.Balance, error) {
	var balance models.Balance

	err := c.get("/balance", &balance)
	if err != nil {
		return nil, fmt.Errorf("failed to get balance: %w", err)
	}

	return &balance, nil
}

// SetBalance sets the balance level using the /balance endpoint
func (c *Client) SetBalance(level int) error {
	if !models.ValidateBalanceLevel(level) {
		return fmt.Errorf("invalid balance level: %d (must be between %d and %d)", level, models.BalanceLevelMin, models.BalanceLevelMax)
	}

	balanceReq, err := models.NewBalanceRequest(level)
	if err != nil {
		return fmt.Errorf("failed to create balance request: %w", err)
	}

	return c.post("/balance", balanceReq)
}

// SetBalanceSafe sets balance with validation and clamping
func (c *Client) SetBalanceSafe(level int) error {
	clampedLevel := models.ClampBalanceLevel(level)
	return c.SetBalance(clampedLevel)
}

// IncreaseBalance increases balance by the specified amount (with safety limits)
func (c *Client) IncreaseBalance(amount int) (*models.Balance, error) {
	currentBalance, err := c.GetBalance()
	if err != nil {
		return nil, fmt.Errorf("failed to get current balance: %w", err)
	}

	newLevel := models.ClampBalanceLevel(currentBalance.GetLevel() + amount)

	err = c.SetBalance(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set balance: %w", err)
	}

	// Return updated balance
	return c.GetBalance()
}

// DecreaseBalance decreases balance by the specified amount (with safety limits)
func (c *Client) DecreaseBalance(amount int) (*models.Balance, error) {
	currentBalance, err := c.GetBalance()
	if err != nil {
		return nil, fmt.Errorf("failed to get current balance: %w", err)
	}

	newLevel := models.ClampBalanceLevel(currentBalance.GetLevel() - amount)

	err = c.SetBalance(newLevel)
	if err != nil {
		return nil, fmt.Errorf("failed to set balance: %w", err)
	}

	// Return updated balance
	return c.GetBalance()
}

// SelectSource selects an audio source using the /select endpoint
func (c *Client) SelectSource(source, sourceAccount string) error {
	// Validate source parameter
	if source == "" {
		return fmt.Errorf("source cannot be empty")
	}

	// Create ContentItem for source selection
	contentItem := &models.ContentItem{
		Source:        source,
		SourceAccount: sourceAccount,
		ItemName:      source, // Use source as default item name
	}

	// For certain sources, we might want to customize the item name
	switch source {
	case "SPOTIFY":
		contentItem.ItemName = "Spotify"
	case "BLUETOOTH":
		contentItem.ItemName = "Bluetooth"
	case "AUX":
		contentItem.ItemName = "AUX IN"
		// The speaker rejects AUX with empty sourceAccount as INVALID_SOURCE.
		if contentItem.SourceAccount == "" {
			contentItem.SourceAccount = "AUX"
		}
	case "TUNEIN":
		contentItem.ItemName = "TuneIn"
	case "PANDORA":
		contentItem.ItemName = "Pandora"
	case "AMAZON":
		contentItem.ItemName = "Amazon Music"
	case "IHEARTRADIO":
		contentItem.ItemName = "iHeartRadio"
	case "STORED_MUSIC":
		contentItem.ItemName = "Stored Music"
	}

	return c.post("/select", contentItem)
}

// SelectSourceFromItem selects an audio source using a SourceItem
func (c *Client) SelectSourceFromItem(sourceItem *models.SourceItem) error {
	if sourceItem == nil {
		return fmt.Errorf("sourceItem cannot be nil")
	}

	return c.SelectSource(sourceItem.Source, sourceItem.SourceAccount)
}

// SelectSpotify is a convenience method to select Spotify source
func (c *Client) SelectSpotify(sourceAccount string) error {
	return c.SelectSource("SPOTIFY", sourceAccount)
}

// SelectBluetooth is a convenience method to select Bluetooth source
func (c *Client) SelectBluetooth() error {
	return c.SelectSource("BLUETOOTH", "")
}

// SelectAux is a convenience method to select AUX input.
func (c *Client) SelectAux() error {
	return c.SelectSource("AUX", "")
}

// SelectTuneIn is a convenience method to select TuneIn source
func (c *Client) SelectTuneIn(sourceAccount string) error {
	return c.SelectSource("TUNEIN", sourceAccount)
}

// SelectPandora is a convenience method to select Pandora source
func (c *Client) SelectPandora(sourceAccount string) error {
	return c.SelectSource("PANDORA", sourceAccount)
}

// SelectContentItem selects content using a ContentItem directly.
// This method allows full control over all ContentItem properties including
// complex location parameters for LOCAL_INTERNET_RADIO streamUrl format.
//
// Example usage for LOCAL_INTERNET_RADIO with streamUrl:
//
//	contentItem := &models.ContentItem{
//	  Source:       "LOCAL_INTERNET_RADIO",
//	  Type:         "stationurl",
//	  Location:     "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio",
//	  IsPresetable: true,
//	  ItemName:     "My Radio Station",
//	  ContainerArt: "https://example.com/art.png",
//	}
//	err := client.SelectContentItem(contentItem)
func (c *Client) SelectContentItem(contentItem *models.ContentItem) error {
	if contentItem == nil {
		return fmt.Errorf("contentItem cannot be nil")
	}

	if contentItem.Source == "" {
		return fmt.Errorf("contentItem source cannot be empty")
	}

	return c.post("/select", contentItem)
}

// SelectLocalInternetRadio is a convenience method to select LOCAL_INTERNET_RADIO content.
// For simple direct stream URLs, use streamURL parameter.
// For complex streamUrl format (with proxy), use the location parameter with full URL.
//
// Example 1 - Direct stream:
//
//	err := client.SelectLocalInternetRadio("https://stream.example.com/radio", "", "My Radio", "")
//
// Example 2 - StreamUrl format with proxy:
//
//	location := "http://contentapi.gmuth.de/station.php?name=MyStation&streamUrl=https://stream.example.com/radio"
//	err := client.SelectLocalInternetRadio(location, "", "My Radio", "https://example.com/art.png")
func (c *Client) SelectLocalInternetRadio(location, sourceAccount, itemName, containerArt string) error {
	if location == "" {
		return fmt.Errorf("location cannot be empty")
	}

	contentItem := &models.ContentItem{
		Source:        "LOCAL_INTERNET_RADIO",
		Type:          "stationurl",
		Location:      location,
		SourceAccount: sourceAccount,
		IsPresetable:  true,
		ItemName:      itemName,
		ContainerArt:  containerArt,
	}

	if itemName == "" {
		contentItem.ItemName = "Internet Radio"
	}

	return c.SelectContentItem(contentItem)
}

// SelectLocalMusic is a convenience method to select LOCAL_MUSIC content.
// This is used for SoundTouch App Media Server content on local computers.
//
// Example:
//
//	err := client.SelectLocalMusic("album:983", "3f205110-4a57-4e91-810a-123456789012", "Welcome to the New", "http://192.0.2.14:8085/v1/albums/983/image")
func (c *Client) SelectLocalMusic(location, sourceAccount, itemName, containerArt string) error {
	if location == "" {
		return fmt.Errorf("location cannot be empty")
	}

	if sourceAccount == "" {
		return fmt.Errorf("sourceAccount cannot be empty for LOCAL_MUSIC")
	}

	contentItem := &models.ContentItem{
		Source:        "LOCAL_MUSIC",
		Type:          "album", // Default type, could be "track", "artist", etc.
		Location:      location,
		SourceAccount: sourceAccount,
		IsPresetable:  true,
		ItemName:      itemName,
		ContainerArt:  containerArt,
	}

	if itemName == "" {
		contentItem.ItemName = "Local Music"
	}

	return c.SelectContentItem(contentItem)
}

// SelectStoredMusic is a convenience method to select STORED_MUSIC content.
// This is used for UPnP/DLNA media servers and NAS libraries.
//
// Example:
//
//	err := client.SelectStoredMusic("6_a2874b5d_4f83d999", "d09708a1-5953-44bc-a413-123456789012/0", "Christmas Album", "")
func (c *Client) SelectStoredMusic(location, sourceAccount, itemName, containerArt string) error {
	if location == "" {
		return fmt.Errorf("location cannot be empty")
	}

	if sourceAccount == "" {
		return fmt.Errorf("sourceAccount cannot be empty for STORED_MUSIC")
	}

	contentItem := &models.ContentItem{
		Source:        "STORED_MUSIC",
		Location:      location,
		SourceAccount: sourceAccount,
		IsPresetable:  true,
		ItemName:      itemName,
		ContainerArt:  containerArt,
	}

	if itemName == "" {
		contentItem.ItemName = "Stored Music"
	}

	return c.SelectContentItem(contentItem)
}

// GetClockTime retrieves the device's current time from the /clockTime endpoint
func (c *Client) GetClockTime() (*models.ClockTime, error) {
	var clockTime models.ClockTime

	err := c.get("/clockTime", &clockTime)
	if err != nil {
		return nil, fmt.Errorf("failed to get clock time: %w", err)
	}

	return &clockTime, nil
}

// SetClockTime sets the device's time via the /clockTime endpoint
func (c *Client) SetClockTime(request *models.ClockTimeRequest) error {
	if err := request.Validate(); err != nil {
		return fmt.Errorf("invalid clock time request: %w", err)
	}

	err := c.post("/clockTime", request)
	if err != nil {
		return fmt.Errorf("failed to set clock time: %w", err)
	}

	return nil
}

// SetClockTimeNow sets the device's time to the current system time
func (c *Client) SetClockTimeNow() error {
	request := models.NewClockTimeRequest(time.Now())
	return c.SetClockTime(request)
}

// GetClockDisplay retrieves clock display settings from the /clockDisplay endpoint
func (c *Client) GetClockDisplay() (*models.ClockDisplay, error) {
	var clockDisplay models.ClockDisplay

	err := c.get("/clockDisplay", &clockDisplay)
	if err != nil {
		return nil, fmt.Errorf("failed to get clock display settings: %w", err)
	}

	return &clockDisplay, nil
}

// SetClockDisplay configures clock display settings via the /clockDisplay endpoint
func (c *Client) SetClockDisplay(request *models.ClockDisplayRequest) error {
	if err := request.Validate(); err != nil {
		return fmt.Errorf("invalid clock display request: %w", err)
	}

	if !request.HasChanges() {
		return fmt.Errorf("no changes specified in clock display request")
	}

	err := c.post("/clockDisplay", request)
	if err != nil {
		return fmt.Errorf("failed to set clock display: %w", err)
	}

	return nil
}

// EnableClockDisplay enables the clock display with default settings
func (c *Client) EnableClockDisplay() error {
	request := models.NewClockDisplayRequest().SetEnabled(true)
	return c.SetClockDisplay(request)
}

// DisableClockDisplay disables the clock display
func (c *Client) DisableClockDisplay() error {
	request := models.NewClockDisplayRequest().SetEnabled(false)
	return c.SetClockDisplay(request)
}

// SetClockDisplayBrightness sets the clock display brightness (0-100)
func (c *Client) SetClockDisplayBrightness(brightness int) error {
	request := models.NewClockDisplayRequest().SetBrightness(brightness)
	return c.SetClockDisplay(request)
}

// SetClockDisplayFormat sets the clock display format (12/24 hour)
func (c *Client) SetClockDisplayFormat(format models.ClockFormat) error {
	request := models.NewClockDisplayRequest().SetFormat(format)
	return c.SetClockDisplay(request)
}

// GetNetworkInfo retrieves network information from the /networkInfo endpoint
func (c *Client) GetNetworkInfo() (*models.NetworkInformation, error) {
	var networkInfo models.NetworkInformation

	err := c.get("/networkInfo", &networkInfo)
	if err != nil {
		return nil, fmt.Errorf("failed to get network info: %w", err)
	}

	return &networkInfo, nil
}

// Ping checks if the device is reachable by calling /info
func (c *Client) Ping() error {
	_, err := c.GetDeviceInfo()
	return err
}

// BaseURL returns the base URL for this client
func (c *Client) BaseURL() string {
	return c.baseURL
}

// Host returns the host for this client
func (c *Client) Host() string {
	return c.baseURL
}

// get performs a GET request and unmarshals the XML response
func (c *Client) get(endpoint string, result interface{}) error {
	url := c.baseURL + endpoint

	req, err := http.NewRequest("GET", url, nil)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("User-Agent", c.userAgent)
	req.Header.Set("Accept", "application/xml")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute request: %w", err)
	}

	defer func() {
		if closeErr := resp.Body.Close(); closeErr != nil {
			// Log the error but don't override the main error
			_ = closeErr // Explicitly ignore the error
		}
	}()

	if resp.StatusCode != http.StatusOK {
		body, _ := io.ReadAll(resp.Body)
		return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return fmt.Errorf("failed to read response body: %w", err)
	}

	// Parse the actual response first
	if err := xml.Unmarshal(body, result); err != nil {
		// Check if it might be an API error response instead
		var apiError models.APIError
		if xmlErr := xml.Unmarshal(body, &apiError); xmlErr == nil && apiError.Message != "" {
			return &apiError
		}

		return fmt.Errorf("failed to unmarshal XML response: %w", err)
	}

	return nil
}

// post performs a POST request with XML body
func (c *Client) post(endpoint string, payload interface{}) error {
	url := c.baseURL + endpoint

	var body io.Reader

	if payload != nil {
		xmlData, err := xml.Marshal(payload)
		if err != nil {
			return fmt.Errorf("failed to marshal XML request: %w", err)
		}

		body = bytes.NewReader(xmlData)
	}

	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("User-Agent", c.userAgent)
	req.Header.Set("Content-Type", "application/xml")
	req.Header.Set("Accept", "application/xml")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute request: %w", err)
	}

	defer func() {
		if closeErr := resp.Body.Close(); closeErr != nil {
			// Log the error but don't override the main error
			_ = closeErr // Explicitly ignore the error
		}
	}()

	if resp.StatusCode != http.StatusOK {
		responseBody, _ := io.ReadAll(resp.Body)

		// Try to parse as ErrorsResponse (speaker error format)
		var errs models.ErrorsResponse
		if xmlErr := xml.Unmarshal(responseBody, &errs); xmlErr == nil && len(errs.Errors) > 0 {
			return &errs
		}

		// Try to parse as APIError (standard format)
		var apiError models.APIError
		if xmlErr := xml.Unmarshal(responseBody, &apiError); xmlErr == nil && apiError.Message != "" {
			return &apiError
		}

		return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(responseBody))
	}

	return nil
}

// postWithResponse performs a POST request with XML body and parses the response
func (c *Client) postWithResponse(endpoint string, payload, result interface{}) error {
	url := c.baseURL + endpoint

	var body io.Reader

	if payload != nil {
		xmlData, err := xml.Marshal(payload)
		if err != nil {
			return fmt.Errorf("failed to marshal XML request: %w", err)
		}

		body = bytes.NewReader(xmlData)
	}

	req, err := http.NewRequest("POST", url, body)
	if err != nil {
		return fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("User-Agent", c.userAgent)
	req.Header.Set("Content-Type", "application/xml")
	req.Header.Set("Accept", "application/xml")

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("failed to execute request: %w", err)
	}

	defer func() {
		if closeErr := resp.Body.Close(); closeErr != nil {
			// Log the error but don't override the main error
			_ = closeErr // Explicitly ignore the error
		}
	}()

	if resp.StatusCode != http.StatusOK {
		responseBody, _ := io.ReadAll(resp.Body)

		// Try to parse as ErrorsResponse (speaker error format)
		var errs models.ErrorsResponse
		if xmlErr := xml.Unmarshal(responseBody, &errs); xmlErr == nil && len(errs.Errors) > 0 {
			return &errs
		}

		// Try to parse as APIError (standard format)
		var apiError models.APIError
		if xmlErr := xml.Unmarshal(responseBody, &apiError); xmlErr == nil && apiError.Message != "" {
			return &apiError
		}

		return fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(responseBody))
	}

	if result != nil {
		responseBody, err := io.ReadAll(resp.Body)
		if err != nil {
			return fmt.Errorf("failed to read response body: %w", err)
		}

		// Parse the actual response first
		if err := xml.Unmarshal(responseBody, result); err != nil {
			// Check if it might be an API error response instead
			var errs models.ErrorsResponse
			if xmlErr := xml.Unmarshal(responseBody, &errs); xmlErr == nil && len(errs.Errors) > 0 {
				return &errs
			}

			var apiError models.APIError
			if xmlErr := xml.Unmarshal(responseBody, &apiError); xmlErr == nil && apiError.Message != "" {
				return &apiError
			}

			return fmt.Errorf("failed to unmarshal XML response: %w", err)
		}
	}

	return nil
}

// GetZone gets the current multiroom zone configuration
func (c *Client) GetZone() (*models.ZoneInfo, error) {
	var zone models.ZoneInfo

	err := c.get("/getZone", &zone)

	return &zone, err
}

// SetZone configures multiroom zone settings
func (c *Client) SetZone(zoneRequest *models.ZoneRequest) error {
	if err := zoneRequest.Validate(); err != nil {
		return fmt.Errorf("invalid zone request: %w", err)
	}

	return c.post("/setZone", zoneRequest)
}

// CreateZone creates a new multiroom zone with the specified master and members
func (c *Client) CreateZone(masterDeviceID string, memberDeviceIDs []string) error {
	zoneRequest := models.NewZoneRequest(masterDeviceID)

	for _, deviceID := range memberDeviceIDs {
		zoneRequest.AddMemberByDeviceID(deviceID)
	}

	return c.SetZone(zoneRequest)
}

// CreateZoneWithIPs creates a new multiroom zone with device IDs and IP addresses
func (c *Client) CreateZoneWithIPs(masterDeviceID string, members map[string]string) error {
	zoneRequest := models.NewZoneRequest(masterDeviceID)

	for deviceID, ipAddress := range members {
		zoneRequest.AddMember(deviceID, ipAddress)
	}

	return c.SetZone(zoneRequest)
}

// AddToZone adds a device to an existing zone
func (c *Client) AddToZone(deviceID, ipAddress string) error {
	// Get current zone configuration
	currentZone, err := c.GetZone()
	if err != nil {
		return fmt.Errorf("failed to get current zone: %w", err)
	}

	// Convert to zone request and add member
	zoneRequest := currentZone.ToZoneRequest()
	zoneRequest.AddMember(deviceID, ipAddress)

	return c.SetZone(zoneRequest)
}

// RemoveFromZone removes a device from the current zone.
//
// It uses the dedicated /removeZoneSlave endpoint rather than rebuilding the
// zone with /setZone and the remaining members: /setZone does not drop a member
// from a multi-member zone (the speaker only goes standalone when the resulting
// member set is empty), so a setZone rebuild silently fails to remove one of
// several members. See #511.
func (c *Client) RemoveFromZone(deviceID string) error {
	// Get current zone configuration
	currentZone, err := c.GetZone()
	if err != nil {
		return fmt.Errorf("failed to get current zone: %w", err)
	}

	if currentZone.IsStandalone() {
		return nil // nothing to remove
	}

	// Carry the member's IP (as the speaker expects) when we know it.
	slaveIP := ""

	for i := range currentZone.Members {
		if currentZone.Members[i].DeviceID == deviceID {
			slaveIP = currentZone.Members[i].IP
			break
		}
	}

	return c.RemoveZoneSlave(currentZone.Master, deviceID, slaveIP)
}

// DissolveZone dissolves the current zone, making all devices standalone
func (c *Client) DissolveZone() error {
	// Get current zone configuration
	currentZone, err := c.GetZone()
	if err != nil {
		return fmt.Errorf("failed to get current zone: %w", err)
	}

	// Create standalone configuration (master only, no members)
	zoneRequest := models.NewZoneRequest(currentZone.Master)

	return c.SetZone(zoneRequest)
}

// IsInZone checks if this device is part of a multiroom zone
func (c *Client) IsInZone() (bool, error) {
	zone, err := c.GetZone()
	if err != nil {
		return false, err
	}

	return !zone.IsStandalone(), nil
}

// GetZoneStatus returns the zone status for this device
func (c *Client) GetZoneStatus() (models.ZoneStatus, error) {
	zone, err := c.GetZone()
	if err != nil {
		return models.ZoneStatusStandalone, err
	}

	// Get device info to determine our device ID
	deviceInfo, err := c.GetDeviceInfo()
	if err != nil {
		return models.ZoneStatusStandalone, fmt.Errorf("failed to get device info: %w", err)
	}

	return zone.GetZoneStatus(deviceInfo.DeviceID), nil
}

// GetZoneMembers returns all devices in the current zone
func (c *Client) GetZoneMembers() ([]string, error) {
	zone, err := c.GetZone()
	if err != nil {
		return nil, err
	}

	return zone.GetAllDeviceIDs(), nil
}

// GetGroup retrieves the current stereo-pair configuration from the device.
// An empty <group/> response is reported as a zero-value Group; callers can
// distinguish with (*Group).IsEmpty().
//
// ST-10 is the only product that supports stereo pairs; on other devices
// the call is harmless but will always return an empty group. The endpoint
// is named /getGroup on the device (mirroring /getZone), even though some
// third-party wikis document it as plain /group.
func (c *Client) GetGroup() (*models.Group, error) {
	var g models.Group

	err := c.get("/getGroup", &g)

	return &g, err
}

// AddGroup creates a new stereo pair on the device addressed by this client,
// which becomes the master. The supplied group must contain both LEFT and
// RIGHT roles; the device assigns the group ID and echoes the full state
// in the response.
func (c *Client) AddGroup(group *models.Group) (*models.Group, error) {
	var result models.Group
	if err := c.postWithResponse("/addGroup", group, &result); err != nil {
		return nil, err
	}

	return &result, nil
}

// UpdateGroup renames or otherwise updates an existing stereo pair. The
// device requires the full group structure on every update, not just the
// changed fields.
func (c *Client) UpdateGroup(group *models.Group) (*models.Group, error) {
	var result models.Group
	if err := c.postWithResponse("/updateGroup", group, &result); err != nil {
		return nil, err
	}

	return &result, nil
}

// RemoveGroup tears down the device's stereo pair. The device returns an
// empty <group/> on success — surfaced here as a non-error nil.
//
// Note: the wiki specifies GET (not DELETE) for this endpoint, so we honour
// that despite the state-mutating semantics.
func (c *Client) RemoveGroup() error {
	var g models.Group

	return c.get("/removeGroup", &g)
}

// SetName sets the device name
func (c *Client) SetName(name string) error {
	nameRequest := models.Name{
		XMLName: xml.Name{Local: "name"},
		Value:   name,
	}

	return c.post("/name", nameRequest)
}

// GetBassCapabilities retrieves the bass capabilities for the device
func (c *Client) GetBassCapabilities() (*models.BassCapabilities, error) {
	var bassCapabilities models.BassCapabilities

	err := c.get("/bassCapabilities", &bassCapabilities)

	return &bassCapabilities, err
}

// GetTrackInfo retrieves track information (duplicate of GetNowPlaying per official API)
// WARNING: This endpoint times out on real devices despite being documented in the official API.
// Use GetNowPlaying() instead for reliable track information.
func (c *Client) GetTrackInfo() (*models.NowPlaying, error) {
	var nowPlaying models.NowPlaying

	err := c.get("/trackInfo", &nowPlaying)

	return &nowPlaying, err
}

// GetAudioDSPControls retrieves the current DSP audio controls
// Only available if audiodspcontrols is listed in the reply to GET /capabilities
func (c *Client) GetAudioDSPControls() (*models.AudioDSPControls, error) {
	// Check if DSP controls are supported by checking capabilities
	capabilities, err := c.GetCapabilities()
	if err != nil {
		return nil, fmt.Errorf("failed to check device capabilities: %w", err)
	}

	// Check if audiodspcontrols capability exists
	if !c.hasCapability(capabilities, "audiodspcontrols") {
		return nil, fmt.Errorf("audiodspcontrols not supported by this device")
	}

	var dspControls models.AudioDSPControls

	err = c.get("/audiodspcontrols", &dspControls)

	return &dspControls, err
}

// SetAudioDSPControls sets the DSP audio controls
// Only available if audiodspcontrols is listed in the reply to GET /capabilities
func (c *Client) SetAudioDSPControls(audioMode string, videoSyncDelay int) error {
	request := &models.AudioDSPControlsRequest{
		AudioMode:           audioMode,
		VideoSyncAudioDelay: videoSyncDelay,
	}

	// Validate against current capabilities
	capabilities, err := c.GetAudioDSPControls()
	if err != nil {
		return fmt.Errorf("DSP controls not supported or available: %w", err)
	}

	if validationErr := request.Validate(capabilities); validationErr != nil {
		return fmt.Errorf("invalid DSP controls request: %w", validationErr)
	}

	return c.post("/audiodspcontrols", request)
}

// SetAudioMode sets only the audio mode (leaving video sync delay unchanged)
func (c *Client) SetAudioMode(mode string) error {
	request := &models.AudioDSPControlsRequest{
		AudioMode: mode,
	}

	// Validate against current capabilities if possible
	capabilities, err := c.GetAudioDSPControls()
	if err == nil {
		if validationErr := request.Validate(capabilities); validationErr != nil {
			return fmt.Errorf("invalid audio mode: %w", validationErr)
		}
	}

	return c.post("/audiodspcontrols", request)
}

// SetVideoSyncAudioDelay sets only the video sync audio delay (leaving audio mode unchanged)
func (c *Client) SetVideoSyncAudioDelay(delay int) error {
	request := &models.AudioDSPControlsRequest{
		VideoSyncAudioDelay: delay,
	}

	if err := request.Validate(nil); err != nil {
		return fmt.Errorf("invalid video sync delay: %w", err)
	}

	return c.post("/audiodspcontrols", request)
}

// GetAudioProductToneControls retrieves the current advanced tone controls (bass/treble)
// Only available if audioproducttonecontrols is listed in the reply to GET /capabilities
func (c *Client) GetAudioProductToneControls() (*models.AudioProductToneControls, error) {
	// Check if tone controls are supported by checking capabilities
	capabilities, err := c.GetCapabilities()
	if err != nil {
		return nil, fmt.Errorf("failed to check device capabilities: %w", err)
	}

	// Check if audioproducttonecontrols capability exists
	if !c.hasCapability(capabilities, "audioproducttonecontrols") {
		return nil, fmt.Errorf("audioproducttonecontrols not supported by this device")
	}

	var toneControls models.AudioProductToneControls

	err = c.get("/audioproducttonecontrols", &toneControls)

	return &toneControls, err
}

// SetAudioProductToneControls sets the advanced tone controls (bass and/or treble)
func (c *Client) SetAudioProductToneControls(bass, treble *int) error {
	request := &models.AudioProductToneControlsRequest{}

	if bass != nil {
		request.Bass = models.NewBassControlValue(*bass)
	}

	if treble != nil {
		request.Treble = models.NewTrebleControlValue(*treble)
	}

	// Validate against current capabilities if possible
	capabilities, err := c.GetAudioProductToneControls()
	if err == nil {
		if validationErr := request.Validate(capabilities); validationErr != nil {
			return fmt.Errorf("invalid tone controls request: %w", validationErr)
		}
	}

	return c.post("/audioproducttonecontrols", request)
}

// SetAdvancedBass sets only the advanced bass control
func (c *Client) SetAdvancedBass(level int) error {
	return c.SetAudioProductToneControls(&level, nil)
}

// SetAdvancedTreble sets only the advanced treble control
func (c *Client) SetAdvancedTreble(level int) error {
	return c.SetAudioProductToneControls(nil, &level)
}

// GetAudioProductLevelControls retrieves the current speaker level controls
// Only available if audioproductlevelcontrols is listed in the reply to GET /capabilities
func (c *Client) GetAudioProductLevelControls() (*models.AudioProductLevelControls, error) {
	// Check if level controls are supported by checking capabilities
	capabilities, err := c.GetCapabilities()
	if err != nil {
		return nil, fmt.Errorf("failed to check device capabilities: %w", err)
	}

	// Check if audioproductlevelcontrols capability exists
	if !c.hasCapability(capabilities, "audioproductlevelcontrols") {
		return nil, fmt.Errorf("audioproductlevelcontrols not supported by this device")
	}

	var levelControls models.AudioProductLevelControls

	err = c.get("/audioproductlevelcontrols", &levelControls)

	return &levelControls, err
}

// SetAudioProductLevelControls sets the speaker level controls
func (c *Client) SetAudioProductLevelControls(frontCenter, rearSurround *int) error {
	request := &models.AudioProductLevelControlsRequest{}

	if frontCenter != nil {
		request.FrontCenterSpeakerLevel = models.NewFrontCenterLevelValue(*frontCenter)
	}

	if rearSurround != nil {
		request.RearSurroundSpeakersLevel = models.NewRearSurroundLevelValue(*rearSurround)
	}

	// Validate against current capabilities if possible
	capabilities, err := c.GetAudioProductLevelControls()
	if err == nil {
		if validationErr := request.Validate(capabilities); validationErr != nil {
			return fmt.Errorf("invalid level controls request: %w", validationErr)
		}
	}

	return c.post("/audioproductlevelcontrols", request)
}

// SetFrontCenterSpeakerLevel sets only the front-center speaker level
func (c *Client) SetFrontCenterSpeakerLevel(level int) error {
	return c.SetAudioProductLevelControls(&level, nil)
}

// SetRearSurroundSpeakersLevel sets only the rear-surround speakers level
func (c *Client) SetRearSurroundSpeakersLevel(level int) error {
	return c.SetAudioProductLevelControls(nil, &level)
}

// AddZoneSlave adds a single device to an existing zone using the official /addZoneSlave endpoint
func (c *Client) AddZoneSlave(masterDeviceID, slaveDeviceID, slaveIP string) error {
	request := models.NewZoneSlaveRequest(masterDeviceID)
	request.AddSlave(slaveDeviceID, slaveIP)

	if err := request.Validate(); err != nil {
		return fmt.Errorf("invalid zone slave request: %w", err)
	}

	return c.post("/addZoneSlave", request)
}

// AddZoneSlaveByDeviceID adds a single device to an existing zone by device ID only
func (c *Client) AddZoneSlaveByDeviceID(masterDeviceID, slaveDeviceID string) error {
	return c.AddZoneSlave(masterDeviceID, slaveDeviceID, "")
}

// RemoveZoneSlave removes a single device from an existing zone using the official /removeZoneSlave endpoint
func (c *Client) RemoveZoneSlave(masterDeviceID, slaveDeviceID, slaveIP string) error {
	request := models.NewZoneSlaveRequest(masterDeviceID)
	request.AddSlave(slaveDeviceID, slaveIP)

	if err := request.Validate(); err != nil {
		return fmt.Errorf("invalid zone slave request: %w", err)
	}

	return c.post("/removeZoneSlave", request)
}

// RemoveZoneSlaveByDeviceID removes a single device from an existing zone by device ID only
func (c *Client) RemoveZoneSlaveByDeviceID(masterDeviceID, slaveDeviceID string) error {
	return c.RemoveZoneSlave(masterDeviceID, slaveDeviceID, "")
}

// RequestToken generates a new bearer token from the device
func (c *Client) RequestToken() (*models.BearerToken, error) {
	var token models.BearerToken

	err := c.get("/requestToken", &token)
	if err != nil {
		return nil, fmt.Errorf("failed to request token: %w", err)
	}

	return &token, nil
}

// Navigate browses content within a source (e.g., browse music libraries, stations)
func (c *Client) Navigate(source, sourceAccount string, startItem, numItems int) (*models.NavigateResponse, error) {
	if source == "" {
		return nil, fmt.Errorf("source cannot be empty")
	}

	if startItem < 1 {
		return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem)
	}

	if numItems < 1 {
		return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems)
	}

	request := models.NewNavigateRequest(source, sourceAccount, startItem, numItems)

	var response models.NavigateResponse

	err := c.postWithResponse("/navigate", request, &response)
	if err != nil {
		return nil, fmt.Errorf("failed to navigate %s: %w", source, err)
	}

	return &response, nil
}

// NavigateWithMenu browses content with menu and sort parameters (e.g., Pandora stations)
func (c *Client) NavigateWithMenu(source, sourceAccount, menu, sort string, startItem, numItems int) (*models.NavigateResponse, error) {
	if source == "" {
		return nil, fmt.Errorf("source cannot be empty")
	}

	if startItem < 1 {
		return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem)
	}

	if numItems < 1 {
		return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems)
	}

	request := models.NewNavigateRequestWithMenu(source, sourceAccount, menu, sort, startItem, numItems)

	var response models.NavigateResponse

	err := c.postWithResponse("/navigate", request, &response)
	if err != nil {
		return nil, fmt.Errorf("failed to navigate %s with menu %s: %w", source, menu, err)
	}

	return &response, nil
}

// NavigateContainer browses a specific container/directory within a source
func (c *Client) NavigateContainer(source, sourceAccount string, startItem, numItems int, containerItem *models.ContentItem) (*models.NavigateResponse, error) {
	if source == "" {
		return nil, fmt.Errorf("source cannot be empty")
	}

	if containerItem == nil {
		return nil, fmt.Errorf("container item cannot be nil")
	}

	if startItem < 1 {
		return nil, fmt.Errorf("startItem must be >= 1, got %d", startItem)
	}

	if numItems < 1 {
		return nil, fmt.Errorf("numItems must be >= 1, got %d", numItems)
	}

	request := models.NewNavigateRequestWithItem(source, sourceAccount, startItem, numItems, containerItem)

	var response models.NavigateResponse

	err := c.postWithResponse("/navigate", request, &response)
	if err != nil {
		return nil, fmt.Errorf("failed to navigate container in %s: %w", source, err)
	}

	return &response, nil
}

// AddStation adds a station to a music service collection and immediately starts playing it
func (c *Client) AddStation(source, sourceAccount, token, name string) error {
	if source == "" {
		return fmt.Errorf("source cannot be empty")
	}

	if token == "" {
		return fmt.Errorf("token cannot be empty")
	}

	if name == "" {
		return fmt.Errorf("station name cannot be empty")
	}

	request := models.NewAddStationRequest(source, sourceAccount, token, name)

	var response models.StationResponse

	err := c.postWithResponse("/addStation", request, &response)
	if err != nil {
		return fmt.Errorf("failed to add station '%s' to %s: %w", name, source, err)
	}

	return nil
}

// RemoveStation removes a station from a music service collection
func (c *Client) RemoveStation(contentItem *models.ContentItem) error {
	if contentItem == nil {
		return fmt.Errorf("content item cannot be nil")
	}

	if contentItem.Source == "" {
		return fmt.Errorf("content item source cannot be empty")
	}

	if contentItem.Location == "" {
		return fmt.Errorf("content item location cannot be empty")
	}

	var response models.StationResponse

	err := c.postWithResponse("/removeStation", contentItem, &response)
	if err != nil {
		return fmt.Errorf("failed to remove station from %s: %w", contentItem.Source, err)
	}

	return nil
}

// GetPandoraStations gets all Pandora radio stations for an account
func (c *Client) GetPandoraStations(sourceAccount string) (*models.NavigateResponse, error) {
	if sourceAccount == "" {
		return nil, fmt.Errorf("pandora source account cannot be empty")
	}

	return c.NavigateWithMenu("PANDORA", sourceAccount, "radioStations", "dateCreated", 1, 100)
}

// GetTuneInStations browses TuneIn stations/content
func (c *Client) GetTuneInStations(sourceAccount string) (*models.NavigateResponse, error) {
	return c.Navigate("TUNEIN", sourceAccount, 1, 100)
}

// GetStoredMusicLibrary browses stored music library
func (c *Client) GetStoredMusicLibrary(sourceAccount string) (*models.NavigateResponse, error) {
	if sourceAccount == "" {
		return nil, fmt.Errorf("stored music source account cannot be empty")
	}

	return c.Navigate("STORED_MUSIC", sourceAccount, 1, 1000)
}

// SearchStation searches for stations/content within a music service
func (c *Client) SearchStation(source, sourceAccount, searchTerm string) (*models.SearchStationResponse, error) {
	if source == "" {
		return nil, fmt.Errorf("source cannot be empty")
	}

	if searchTerm == "" {
		return nil, fmt.Errorf("search term cannot be empty")
	}

	request := models.NewSearchStationRequest(source, sourceAccount, searchTerm)

	var response models.SearchStationResponse

	err := c.postWithResponse("/searchStation", request, &response)
	if err != nil {
		return nil, fmt.Errorf("failed to search stations in %s: %w", source, err)
	}

	return &response, nil
}

// SearchPandoraStations searches for Pandora stations by artist/song name
func (c *Client) SearchPandoraStations(sourceAccount, searchTerm string) (*models.SearchStationResponse, error) {
	if sourceAccount == "" {
		return nil, fmt.Errorf("pandora source account cannot be empty")
	}

	return c.SearchStation("PANDORA", sourceAccount, searchTerm)
}

// SearchTuneInStations searches for TuneIn stations/content
func (c *Client) SearchTuneInStations(searchTerm string) (*models.SearchStationResponse, error) {
	return c.SearchStation("TUNEIN", "", searchTerm)
}

// SearchSpotifyContent searches for Spotify content (playlists, tracks, etc.)
func (c *Client) SearchSpotifyContent(sourceAccount, searchTerm string) (*models.SearchStationResponse, error) {
	if sourceAccount == "" {
		return nil, fmt.Errorf("spotify source account cannot be empty")
	}

	return c.SearchStation("SPOTIFY", sourceAccount, searchTerm)
}

// hasCapability checks if a capability is present in the device capabilities
func (c *Client) hasCapability(capabilities *models.Capabilities, capability string) bool {
	// Convert capabilities to string and check if it contains the capability
	// This is a simplified check - in practice, you'd parse the actual capabilities XML structure
	capStr := fmt.Sprintf("%+v", capabilities)
	return strings.Contains(capStr, capability)
}

// PlayTTS plays a Text-To-Speech message using Google TTS on the speaker
func (c *Client) PlayTTS(text, appKey, language string, volume ...int) error {
	playInfo := models.NewTTSPlayInfo(text, appKey, language, volume...)

	if err := playInfo.Validate(); err != nil {
		return fmt.Errorf("invalid TTS request: %w", err)
	}

	return c.postPlayInfo(playInfo)
}

// PlayURL plays audio content from a URL on the speaker
func (c *Client) PlayURL(url, appKey, service, message, reason string, volume ...int) error {
	playInfo := models.NewURLPlayInfo(url, appKey, service, message, reason, volume...)

	if err := playInfo.Validate(); err != nil {
		return fmt.Errorf("invalid URL play request: %w", err)
	}

	return c.postPlayInfo(playInfo)
}

// PlayCustom plays custom content using a PlayInfo configuration
func (c *Client) PlayCustom(playInfo *models.PlayInfo) error {
	if err := playInfo.Validate(); err != nil {
		return fmt.Errorf("invalid play request: %w", err)
	}

	return c.postPlayInfo(playInfo)
}

// PlayNotificationBeep plays a notification beep on the device
func (c *Client) PlayNotificationBeep() error {
	return c.PlayNotification("")
}

// PlayNotification plays a notification. If a non-empty local path is provided,
// it will be sent as XML body to play that specific device-local PCM file.
// When path is empty, the device's default beep is triggered.
func (c *Client) PlayNotification(path string) error {
	// Empty path -> trigger default beep via GET
	if strings.TrimSpace(path) == "" {
		var status models.StationResponse
		return c.get("/playNotification", &status)
	}

	// Non-empty path -> POST minimal XML payload as required by the device
	payload := struct {
		XMLName    xml.Name `xml:"audioSource"`
		PathToFile string   `xml:"pathToFile,attr"`
	}{
		XMLName:    xml.Name{Local: "audioSource"},
		PathToFile: path,
	}

	return c.post("/playNotification", payload)
}

// Introspect retrieves introspect data for a specified music service
func (c *Client) Introspect(source, sourceAccount string) (*models.IntrospectResponse, error) {
	if source == "" {
		return nil, fmt.Errorf("source cannot be empty")
	}

	request := models.NewIntrospectRequest(source, sourceAccount)

	var response models.IntrospectResponse

	err := c.postWithResponse("/introspect", request, &response)
	if err != nil {
		return nil, fmt.Errorf("failed to get introspect data for %s: %w", source, err)
	}

	return &response, nil
}

// IntrospectSpotify is a convenience method to get introspect data for Spotify
func (c *Client) IntrospectSpotify(sourceAccount string) (*models.IntrospectResponse, error) {
	return c.Introspect("SPOTIFY", sourceAccount)
}

// GetRecents retrieves recently played content from the device
func (c *Client) GetRecents() (*models.RecentsResponse, error) {
	var response models.RecentsResponse

	err := c.get("/recents", &response)
	if err != nil {
		return nil, fmt.Errorf("failed to get recent items: %w", err)
	}

	return &response, nil
}

// postPlayInfo sends a PlayInfo request to the /speaker endpoint
func (c *Client) postPlayInfo(playInfo *models.PlayInfo) error {
	return c.post("/speaker", playInfo)
}

// SetMusicServiceAccount adds or updates a music service account
func (c *Client) SetMusicServiceAccount(credentials *models.MusicServiceCredentials) error {
	if credentials == nil {
		return fmt.Errorf("credentials cannot be nil")
	}

	if err := credentials.Validate(); err != nil {
		return fmt.Errorf("invalid credentials: %w", err)
	}

	var response models.MusicServiceAccountResponse

	err := c.postWithResponse("/setMusicServiceAccount", credentials, &response)
	if err != nil {
		return fmt.Errorf("failed to set music service account for %s: %w", credentials.Source, err)
	}

	if !response.IsSuccess() {
		return fmt.Errorf("music service account operation failed: unexpected response %s", response.Status)
	}

	return nil
}

// SetMusicServiceOAuthAccount adds or updates a music service account using OAuth credentials
func (c *Client) SetMusicServiceOAuthAccount(credentials *models.OAuthCredentials) error {
	if credentials == nil {
		return fmt.Errorf("credentials cannot be nil")
	}

	var response models.MusicServiceAccountResponse

	// Note: Modern firmware uses /setMusicServiceOAuthAccount, but we reuse the success logic
	err := c.postWithResponse("/setMusicServiceOAuthAccount", credentials, &response)
	if err != nil {
		return fmt.Errorf("failed to set music service OAuth account for %s: %w", credentials.Source, err)
	}

	// The speaker returns /setMusicServiceOAuthAccount on success
	if response.Status != "/setMusicServiceOAuthAccount" {
		return fmt.Errorf("music service OAuth account operation failed: unexpected response %s", response.Status)
	}

	return nil
}

// NotifySourcesUpdated notifies the device that sources have been updated in Marge
func (c *Client) NotifySourcesUpdated(deviceID string) error {
	notification := models.NewSourcesUpdatedNotification(deviceID)

	var response models.MusicServiceAccountResponse

	err := c.postWithResponse("/notification", notification, &response)
	if err != nil {
		return fmt.Errorf("failed to send sources updated notification: %w", err)
	}

	if response.Status != "/notification" {
		return fmt.Errorf("sources updated notification failed: unexpected response %s", response.Status)
	}

	return nil
}

// RemoveMusicServiceAccount removes an existing music service account
func (c *Client) RemoveMusicServiceAccount(credentials *models.MusicServiceCredentials) error {
	if credentials == nil {
		return fmt.Errorf("credentials cannot be nil")
	}

	if credentials.Source == "" {
		return fmt.Errorf("source cannot be empty")
	}

	if credentials.User == "" {
		return fmt.Errorf("user cannot be empty")
	}

	// For removal, ensure password is empty
	removalCredentials := &models.MusicServiceCredentials{
		Source:      credentials.Source,
		DisplayName: credentials.DisplayName,
		User:        credentials.User,
		Pass:        "", // Empty password indicates removal
	}

	var response models.MusicServiceAccountResponse

	err := c.postWithResponse("/removeMusicServiceAccount", removalCredentials, &response)
	if err != nil {
		return fmt.Errorf("failed to remove music service account for %s: %w", credentials.Source, err)
	}

	if !response.IsSuccess() {
		return fmt.Errorf("music service account removal failed: unexpected response %s", response.Status)
	}

	return nil
}

// AddSpotifyAccount adds a Spotify Premium account
func (c *Client) AddSpotifyAccount(user, password string) error {
	credentials := models.NewSpotifyCredentials(user, password)
	return c.SetMusicServiceAccount(credentials)
}

// RemoveSpotifyAccount removes a Spotify account
func (c *Client) RemoveSpotifyAccount(user string) error {
	credentials := models.NewSpotifyCredentials(user, "")
	return c.RemoveMusicServiceAccount(credentials)
}

// AddPandoraAccount adds a Pandora account
func (c *Client) AddPandoraAccount(user, password string) error {
	credentials := models.NewPandoraCredentials(user, password)
	return c.SetMusicServiceAccount(credentials)
}

// RemovePandoraAccount removes a Pandora account
func (c *Client) RemovePandoraAccount(user string) error {
	credentials := models.NewPandoraCredentials(user, "")
	return c.RemoveMusicServiceAccount(credentials)
}

// AddStoredMusicAccount adds a STORED_MUSIC (NAS/UPnP) account
func (c *Client) AddStoredMusicAccount(user, displayName string) error {
	credentials := models.NewStoredMusicCredentials(user, displayName)
	return c.SetMusicServiceAccount(credentials)
}

// RemoveStoredMusicAccount removes a STORED_MUSIC account
func (c *Client) RemoveStoredMusicAccount(user, displayName string) error {
	credentials := models.NewStoredMusicCredentials(user, displayName)
	return c.RemoveMusicServiceAccount(credentials)
}

// AddAmazonMusicAccount adds an Amazon Music account
func (c *Client) AddAmazonMusicAccount(user, password string) error {
	credentials := models.NewAmazonMusicCredentials(user, password)
	return c.SetMusicServiceAccount(credentials)
}

// RemoveAmazonMusicAccount removes an Amazon Music account
func (c *Client) RemoveAmazonMusicAccount(user string) error {
	credentials := models.NewAmazonMusicCredentials(user, "")
	return c.RemoveMusicServiceAccount(credentials)
}

// AddDeezerAccount adds a Deezer Premium account
func (c *Client) AddDeezerAccount(user, password string) error {
	credentials := models.NewDeezerCredentials(user, password)
	return c.SetMusicServiceAccount(credentials)
}

// RemoveDeezerAccount removes a Deezer account
func (c *Client) RemoveDeezerAccount(user string) error {
	credentials := models.NewDeezerCredentials(user, "")
	return c.RemoveMusicServiceAccount(credentials)
}

// AddIHeartRadioAccount adds an iHeartRadio account
func (c *Client) AddIHeartRadioAccount(user, password string) error {
	credentials := models.NewIHeartRadioCredentials(user, password)
	return c.SetMusicServiceAccount(credentials)
}

// RemoveIHeartRadioAccount removes an iHeartRadio account
func (c *Client) RemoveIHeartRadioAccount(user string) error {
	credentials := models.NewIHeartRadioCredentials(user, "")
	return c.RemoveMusicServiceAccount(credentials)
}
