// Package mpv spricht ueber den JSON-IPC-Socket mit einem laufenden mpv.
package mpv

import (
	"bufio"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"sync"
	"time"
)

// ErrClosed wird geliefert, wenn die Verbindung nicht mehr steht.
var ErrClosed = errors.New("mpv: verbindung geschlossen")

// Event ist eine von mpv gesendete Statusmeldung.
type Event struct {
	Name  string          `json:"event"`
	ID    int             `json:"id"`
	Data  json.RawMessage `json:"data"`
	Name2 string          `json:"name"` // bei property-change: Name der Eigenschaft

	// Raw ist die vollstaendige Meldung - fuer Felder, die je nach Ereignis
	// unterschiedlich heissen (etwa "reason" bei end-file).
	Raw json.RawMessage `json:"-"`
}

type response struct {
	Error     string          `json:"error"`
	Data      json.RawMessage `json:"data"`
	RequestID int             `json:"request_id"`
}

// Client ist eine Verbindung zum IPC-Socket von mpv.
type Client struct {
	conn net.Conn

	mu      sync.Mutex
	nextID  int
	pending map[int]chan response
	closed  bool

	events chan Event
	done   chan struct{}
}

// Dial verbindet sich mit dem Socket und wartet dabei bis zu timeout, weil mpv
// den Socket erst kurz nach dem Start anlegt.
func Dial(socketPath string, timeout time.Duration) (*Client, error) {
	deadline := time.Now().Add(timeout)
	var lastErr error
	for {
		conn, err := net.Dial("unix", socketPath)
		if err == nil {
			c := &Client{
				conn:    conn,
				pending: make(map[int]chan response),
				events:  make(chan Event, 64),
				done:    make(chan struct{}),
			}
			go c.readLoop()
			return c, nil
		}
		lastErr = err
		if time.Now().After(deadline) {
			return nil, fmt.Errorf("mpv-socket %s nicht erreichbar: %w", socketPath, lastErr)
		}
		time.Sleep(150 * time.Millisecond)
	}
}

// Events liefert den Kanal mit den Statusmeldungen von mpv.
func (c *Client) Events() <-chan Event { return c.events }

// Done wird geschlossen, sobald die Verbindung abbricht.
func (c *Client) Done() <-chan struct{} { return c.done }

// Close beendet die Verbindung.
func (c *Client) Close() error {
	c.mu.Lock()
	if c.closed {
		c.mu.Unlock()
		return nil
	}
	c.closed = true
	c.mu.Unlock()
	return c.conn.Close()
}

func (c *Client) readLoop() {
	defer func() {
		c.mu.Lock()
		c.closed = true
		for id, ch := range c.pending {
			close(ch)
			delete(c.pending, id)
		}
		c.mu.Unlock()
		close(c.done)
		close(c.events)
	}()

	sc := bufio.NewScanner(c.conn)
	sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
	for sc.Scan() {
		line := sc.Bytes()
		if len(line) == 0 {
			continue
		}
		// Eine Zeile ist entweder ein Event oder die Antwort auf ein Kommando.
		var probe struct {
			Event     string `json:"event"`
			RequestID *int   `json:"request_id"`
		}
		if json.Unmarshal(line, &probe) != nil {
			continue
		}
		if probe.Event != "" {
			var ev Event
			if json.Unmarshal(line, &ev) == nil {
				ev.Raw = append(json.RawMessage(nil), line...)
				select {
				case c.events <- ev:
				default: // Ereignisse sind unkritisch: lieber verwerfen als blockieren
				}
			}
			continue
		}
		if probe.RequestID == nil {
			continue
		}
		var resp response
		if json.Unmarshal(line, &resp) != nil {
			continue
		}
		c.mu.Lock()
		ch, ok := c.pending[resp.RequestID]
		if ok {
			delete(c.pending, resp.RequestID)
		}
		c.mu.Unlock()
		if ok {
			ch <- resp
			close(ch)
		}
	}
}

// Command schickt ein Kommando an mpv und wartet auf die Antwort.
func (c *Client) Command(args ...any) (json.RawMessage, error) {
	c.mu.Lock()
	if c.closed {
		c.mu.Unlock()
		return nil, ErrClosed
	}
	c.nextID++
	id := c.nextID
	ch := make(chan response, 1)
	c.pending[id] = ch
	c.mu.Unlock()

	payload, err := json.Marshal(map[string]any{"command": args, "request_id": id})
	if err != nil {
		c.forget(id)
		return nil, err
	}
	if err := c.conn.SetWriteDeadline(time.Now().Add(3 * time.Second)); err == nil {
		defer c.conn.SetWriteDeadline(time.Time{})
	}
	if _, err := c.conn.Write(append(payload, '\n')); err != nil {
		c.forget(id)
		return nil, err
	}

	select {
	case resp, ok := <-ch:
		if !ok {
			return nil, ErrClosed
		}
		if resp.Error != "" && resp.Error != "success" {
			return resp.Data, fmt.Errorf("mpv: %s", resp.Error)
		}
		return resp.Data, nil
	case <-time.After(5 * time.Second):
		c.forget(id)
		return nil, fmt.Errorf("mpv: zeitueberschreitung bei %v", args)
	}
}

func (c *Client) forget(id int) {
	c.mu.Lock()
	delete(c.pending, id)
	c.mu.Unlock()
}

// SetProperty setzt eine mpv-Eigenschaft.
func (c *Client) SetProperty(name string, value any) error {
	_, err := c.Command("set_property", name, value)
	return err
}

// GetString liest eine Eigenschaft als Zeichenkette.
func (c *Client) GetString(name string) (string, error) {
	data, err := c.Command("get_property", name)
	if err != nil {
		return "", err
	}
	var s string
	if json.Unmarshal(data, &s) != nil {
		return string(data), nil
	}
	return s, nil
}

// GetBool liest eine Eigenschaft als Wahrheitswert.
func (c *Client) GetBool(name string) (bool, error) {
	data, err := c.Command("get_property", name)
	if err != nil {
		return false, err
	}
	var b bool
	return b, json.Unmarshal(data, &b)
}

// GetInt liest eine Eigenschaft als Ganzzahl.
func (c *Client) GetInt(name string) (int, error) {
	data, err := c.Command("get_property", name)
	if err != nil {
		return 0, err
	}
	var n float64
	if err := json.Unmarshal(data, &n); err != nil {
		return 0, err
	}
	return int(n), nil
}

// ObserveProperty laesst mpv Aenderungen an einer Eigenschaft melden.
func (c *Client) ObserveProperty(id int, name string) error {
	_, err := c.Command("observe_property", id, name)
	return err
}
