// Package hksv provides a reusable HomeKit Secure Video server library.
//
// It implements HKSV recording (fMP4 over HDS DataStream), motion detection,
// and integrates with the HAP protocol for HomeKit pairing and communication.
//
// Usage:
//
//	srv, err := hksv.NewServer(hksv.Config{
//	    StreamName: "camera1",
//	    Pin:        "27041991",
//	    HKSV:       true,
//	    MotionMode: "detect",
//	    Streams:    myStreamProvider,
//	    Logger:     logger,
//	    Port:       8080,
//	})
//	// Register srv.Handle as HTTP handler for HAP paths
//	// Advertise srv.MDNSEntry() via mDNS
//
// Author: Sergei "svk" Krashevich <svk@svk.su>
package hksv

import (
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"net/url"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/AlexxIT/go2rtc/pkg/core"
	"github.com/AlexxIT/go2rtc/pkg/hap"
	"github.com/AlexxIT/go2rtc/pkg/hap/camera"
	"github.com/AlexxIT/go2rtc/pkg/hap/hds"
	"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
	"github.com/AlexxIT/go2rtc/pkg/homekit"
	"github.com/AlexxIT/go2rtc/pkg/mdns"
	"github.com/rs/zerolog"
)

// StreamProvider provides access to media streams.
// The host application implements this to connect the HKSV library
// to its own stream management system.
type StreamProvider interface {
	// AddConsumer connects a consumer to the named stream.
	AddConsumer(streamName string, consumer core.Consumer) error
	// RemoveConsumer disconnects a consumer from the named stream.
	RemoveConsumer(streamName string, consumer core.Consumer)
}

// PairingStore persists HAP pairing data.
type PairingStore interface {
	SavePairings(streamName string, pairings []string) error
}

// SnapshotProvider generates JPEG snapshots for HomeKit /resource requests.
type SnapshotProvider interface {
	GetSnapshot(streamName string, width, height int) ([]byte, error)
}

// LiveStreamHandler handles live-streaming requests (SetupEndpoints, SelectedStreamConfiguration).
// Implementation is external because it depends on SRTP.
type LiveStreamHandler interface {
	// SetupEndpoints handles a SetupEndpoints request (ch118).
	// Returns the response to store as characteristic value.
	SetupEndpoints(conn net.Conn, offer *camera.SetupEndpointsRequest) (any, error)

	// GetEndpointsResponse returns the current endpoints response (for GET requests).
	GetEndpointsResponse() any

	// StartStream starts RTP streaming with the given configuration (ch117 command=start).
	// The connTracker is used to register/unregister the live stream connection.
	StartStream(streamName string, conf *camera.SelectedStreamConfiguration, connTracker ConnTracker) error

	// StopStream stops a stream matching the given session ID.
	StopStream(sessionID string, connTracker ConnTracker) error
}

// ConnTracker allows the live stream handler to track connections on the server.
type ConnTracker interface {
	AddConn(v any)
	DelConn(v any)
}

// Config for creating an HKSV server.
type Config struct {
	StreamName      string
	Pin             string   // HomeKit pairing PIN (e.g., "27041991")
	Name            string   // mDNS display name (auto-generated if empty)
	DeviceID        string   // MAC-like device ID (auto-generated if empty)
	DevicePrivate   string   // ed25519 private key hex (auto-generated if empty)
	CategoryID      string   // "camera" or "doorbell"
	Pairings        []string // pre-existing pairings
	ProxyURL        string   // if set, acts as transparent proxy (no local accessory)
	HKSV            bool
	MotionMode      string  // "api", "continuous", "detect"
	MotionThreshold float64 // ratio threshold for "detect" mode (default 2.0)
	Speaker         *bool   // include Speaker service for 2-way audio (default false)
	UserAgent       string  // for mDNS TXTModel field
	Version         string  // for accessory firmware version

	// Dependencies (injected by host)
	Streams    StreamProvider
	Store      PairingStore      // optional, nil = no persistence
	Snapshots  SnapshotProvider  // optional, nil = no snapshots
	LiveStream LiveStreamHandler // optional, nil = HKSV only (no live streaming)
	Logger     zerolog.Logger

	// Network
	Port uint16 // HAP HTTP port
}

// Server is a complete HKSV camera server.
type Server struct {
	hap  *hap.Server
	mdns *mdns.ServiceEntry
	log  zerolog.Logger

	pairings []string
	conns    []any
	mu       sync.Mutex

	accessory *hap.Accessory
	setupID   string
	stream    string // stream name

	proxyURL string // transparent proxy URL

	// Injected dependencies
	streams    StreamProvider
	store      PairingStore
	snapshots  SnapshotProvider
	liveStream LiveStreamHandler

	// HKSV fields
	motionMode       string
	motionThreshold  float64
	motionDetector   *MotionDetector
	hksvSession      *hksvSession
	continuousMotion bool
	preparedConsumer *HKSVConsumer
}

// NewServer creates a new HKSV server with the given configuration.
func NewServer(cfg Config) (*Server, error) {
	if cfg.Pin == "" {
		cfg.Pin = "27041991"
	}

	pin, err := hap.SanitizePin(cfg.Pin)
	if err != nil {
		return nil, fmt.Errorf("hksv: invalid pin: %w", err)
	}

	deviceID := CalcDeviceID(cfg.DeviceID, cfg.StreamName)
	name := CalcName(cfg.Name, deviceID)
	setupID := CalcSetupID(cfg.StreamName)

	srv := &Server{
		stream:          cfg.StreamName,
		pairings:        cfg.Pairings,
		setupID:         setupID,
		log:             cfg.Logger,
		streams:         cfg.Streams,
		store:           cfg.Store,
		snapshots:       cfg.Snapshots,
		liveStream:      cfg.LiveStream,
		motionMode:      cfg.MotionMode,
		motionThreshold: cfg.MotionThreshold,
	}

	srv.hap = &hap.Server{
		Pin:             pin,
		DeviceID:        deviceID,
		DevicePrivate:   CalcDevicePrivate(cfg.DevicePrivate, cfg.StreamName),
		GetClientPublic: srv.GetPair,
	}

	categoryID := CalcCategoryID(cfg.CategoryID)

	srv.mdns = &mdns.ServiceEntry{
		Name: name,
		Port: cfg.Port,
		Info: map[string]string{
			hap.TXTConfigNumber: "1",
			hap.TXTFeatureFlags: "0",
			hap.TXTDeviceID:     deviceID,
			hap.TXTModel:        cfg.UserAgent,
			hap.TXTProtoVersion: "1.1",
			hap.TXTStateNumber:  "1",
			hap.TXTStatusFlags:  hap.StatusNotPaired,
			hap.TXTCategory:     categoryID,
			hap.TXTSetupHash:    hap.SetupHash(setupID, deviceID),
		},
	}

	srv.UpdateStatus()

	if cfg.ProxyURL != "" {
		// Proxy mode: no local accessory
		srv.proxyURL = cfg.ProxyURL
	} else if cfg.HKSV {
		if srv.motionThreshold <= 0 {
			srv.motionThreshold = defaultThreshold
		}
		srv.log.Debug().Str("stream", cfg.StreamName).Str("motion", cfg.MotionMode).
			Float64("threshold", srv.motionThreshold).Msg("[hksv] HKSV mode")

		if cfg.CategoryID == "doorbell" {
			srv.accessory = camera.NewHKSVDoorbellAccessory("AlexxIT", "go2rtc", name, "-", cfg.Version)
		} else {
			srv.accessory = camera.NewHKSVAccessory("AlexxIT", "go2rtc", name, "-", cfg.Version)
		}
	} else {
		srv.accessory = camera.NewAccessory("AlexxIT", "go2rtc", name, "-", cfg.Version)
	}

	// Remove Speaker service unless explicitly enabled (default: disabled)
	if (cfg.Speaker == nil || !*cfg.Speaker) && srv.accessory != nil {
		filtered := srv.accessory.Services[:0]
		for _, svc := range srv.accessory.Services {
			if svc.Type != "113" { // 113 = Speaker
				filtered = append(filtered, svc)
			}
		}
		srv.accessory.Services = filtered
		srv.accessory.InitIID() // recalculate IIDs
	}

	return srv, nil
}

// MDNSEntry returns the mDNS service entry for advertisement.
func (s *Server) MDNSEntry() *mdns.ServiceEntry {
	return s.mdns
}

// Accessory returns the HAP accessory.
func (s *Server) Accessory() *hap.Accessory {
	return s.accessory
}

// StreamName returns the configured stream name.
func (s *Server) StreamName() string {
	return s.stream
}

func (s *Server) MarshalJSON() ([]byte, error) {
	v := struct {
		Name       string `json:"name"`
		DeviceID   string `json:"device_id"`
		Paired     int    `json:"paired,omitempty"`
		CategoryID string `json:"category_id,omitempty"`
		SetupCode  string `json:"setup_code,omitempty"`
		SetupID    string `json:"setup_id,omitempty"`
		Conns      []any  `json:"connections,omitempty"`
	}{
		Name:       s.mdns.Name,
		DeviceID:   s.mdns.Info[hap.TXTDeviceID],
		CategoryID: s.mdns.Info[hap.TXTCategory],
		Paired:     len(s.pairings),
		Conns:      s.conns,
	}
	if v.Paired == 0 {
		v.SetupCode = s.hap.Pin
		v.SetupID = s.setupID
	}
	return json.Marshal(v)
}

// Handle processes an incoming HAP connection (called from your HTTP server).
func (s *Server) Handle(w http.ResponseWriter, r *http.Request) {
	conn, rw, err := w.(http.Hijacker).Hijack()
	if err != nil {
		return
	}

	defer conn.Close()

	// Fix reading from Body after Hijack.
	r.Body = io.NopCloser(rw)

	switch r.RequestURI {
	case hap.PathPairSetup:
		id, key, err := s.hap.PairSetup(r, rw)
		if err != nil {
			s.log.Error().Err(err).Caller().Send()
			return
		}

		s.AddPair(id, key, hap.PermissionAdmin)

	case hap.PathPairVerify:
		id, key, err := s.hap.PairVerify(r, rw)
		if err != nil {
			s.log.Debug().Err(err).Caller().Send()
			return
		}

		s.log.Debug().Str("stream", s.stream).Str("client_id", id).Msgf("[hksv] %s: new conn", conn.RemoteAddr())

		controller, err := hap.NewConn(conn, rw, key, false)
		if err != nil {
			s.log.Error().Err(err).Caller().Send()
			return
		}

		s.AddConn(controller)
		defer s.DelConn(controller)

		// start motion on first Home Hub connection
		switch s.motionMode {
		case "detect":
			go s.startMotionDetector()
		case "continuous":
			go s.prepareHKSVConsumer()
			go s.startContinuousMotion()
		}

		var handler homekit.HandlerFunc

		switch {
		case s.accessory != nil:
			handler = homekit.ServerHandler(s)
		case s.proxyURL != "":
			client, err := hap.Dial(s.proxyURL)
			if err != nil {
				s.log.Error().Err(err).Caller().Send()
				return
			}
			handler = homekit.ProxyHandler(s, client.Conn)
		}

		s.log.Debug().Str("stream", s.stream).Msgf("[hksv] handler started for %s", conn.RemoteAddr())

		if err = handler(controller); err != nil {
			if errors.Is(err, io.EOF) || isClosedConnErr(err) {
				s.log.Debug().Str("stream", s.stream).Msgf("[hksv] %s: connection closed", conn.RemoteAddr())
			} else {
				s.log.Error().Err(err).Str("stream", s.stream).Caller().Send()
			}
			return
		}
	}
}

// AddConn registers a connection for tracking.
func (s *Server) AddConn(v any) {
	s.log.Trace().Str("stream", s.stream).Msgf("[hksv] add conn %s", connLabel(v))
	s.mu.Lock()
	s.conns = append(s.conns, v)
	s.mu.Unlock()
}

// DelConn unregisters a connection.
func (s *Server) DelConn(v any) {
	s.log.Trace().Str("stream", s.stream).Msgf("[hksv] del conn %s", connLabel(v))
	s.mu.Lock()
	if i := slices.Index(s.conns, v); i >= 0 {
		s.conns = slices.Delete(s.conns, i, i+1)
	}
	s.mu.Unlock()
}

// connLabel returns a short human-readable label for a connection.
func connLabel(v any) string {
	switch v := v.(type) {
	case *hap.Conn:
		return "hap " + v.RemoteAddr().String()
	case *hds.Conn:
		return "hds " + v.RemoteAddr().String()
	}
	if s, ok := v.(fmt.Stringer); ok {
		return s.String()
	}
	return fmt.Sprintf("%T", v)
}

func (s *Server) UpdateStatus() {
	if len(s.pairings) == 0 {
		s.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired
	} else {
		s.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired
	}
}

func (s *Server) pairIndex(id string) int {
	id = "client_id=" + id
	for i, pairing := range s.pairings {
		if strings.HasPrefix(pairing, id) {
			return i
		}
	}
	return -1
}

func (s *Server) GetPair(id string) []byte {
	s.mu.Lock()
	defer s.mu.Unlock()

	if i := s.pairIndex(id); i >= 0 {
		query, _ := url.ParseQuery(s.pairings[i])
		b, _ := hex.DecodeString(query.Get("client_public"))
		return b
	}
	return nil
}

func (s *Server) AddPair(id string, public []byte, permissions byte) {
	s.log.Debug().Str("stream", s.stream).Msgf("[hksv] add pair id=%s public=%x perm=%d", id, public, permissions)

	s.mu.Lock()
	if s.pairIndex(id) < 0 {
		s.pairings = append(s.pairings, fmt.Sprintf(
			"client_id=%s&client_public=%x&permissions=%d", id, public, permissions,
		))
		s.UpdateStatus()
		s.savePairings()
	}
	s.mu.Unlock()
}

func (s *Server) DelPair(id string) {
	s.log.Debug().Str("stream", s.stream).Msgf("[hksv] del pair id=%s", id)

	s.mu.Lock()
	if i := s.pairIndex(id); i >= 0 {
		s.pairings = append(s.pairings[:i], s.pairings[i+1:]...)
		s.UpdateStatus()
		s.savePairings()
	}
	s.mu.Unlock()
}

func (s *Server) savePairings() {
	if s.store != nil {
		if err := s.store.SavePairings(s.stream, s.pairings); err != nil {
			s.log.Error().Err(err).Msgf("[hksv] can't save %s pairings=%v", s.stream, s.pairings)
		}
	}
}

func (s *Server) GetAccessories(_ net.Conn) []*hap.Accessory {
	s.log.Trace().Str("stream", s.stream).Msg("[hksv] GET /accessories")
	if s.log.Trace().Enabled() {
		if b, err := json.Marshal(s.accessory); err == nil {
			s.log.Trace().Str("stream", s.stream).Str("accessory", string(b)).Msg("[hksv] accessory JSON")
		}
	}
	return []*hap.Accessory{s.accessory}
}

func (s *Server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {
	s.log.Trace().Str("stream", s.stream).Msgf("[hksv] get char aid=%d iid=0x%x", aid, iid)

	char := s.accessory.GetCharacterByID(iid)
	if char == nil {
		s.log.Warn().Msgf("[hksv] get unknown characteristic: %d", iid)
		return nil
	}

	switch char.Type {
	case camera.TypeSetupEndpoints:
		if s.liveStream != nil {
			return s.liveStream.GetEndpointsResponse()
		}
		return nil
	}

	return char.Value
}

func (s *Server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {
	s.log.Trace().Str("stream", s.stream).Msgf("[hksv] set char aid=%d iid=0x%x value=%v", aid, iid, value)

	char := s.accessory.GetCharacterByID(iid)
	if char == nil {
		s.log.Warn().Msgf("[hksv] set unknown characteristic: %d", iid)
		return
	}

	switch char.Type {
	case camera.TypeSetupEndpoints:
		if s.liveStream == nil {
			return
		}
		var offer camera.SetupEndpointsRequest
		if err := tlv8.UnmarshalBase64(value, &offer); err != nil {
			return
		}
		resp, err := s.liveStream.SetupEndpoints(conn, &offer)
		if err != nil {
			s.log.Error().Err(err).Msg("[hksv] setup endpoints failed")
			return
		}
		// Keep the latest response in characteristic value for write-response (r=true)
		// and subsequent GET /characteristics reads.
		char.Value = resp

	case camera.TypeSelectedStreamConfiguration:
		if s.liveStream == nil {
			return
		}
		var conf camera.SelectedStreamConfiguration
		if err := tlv8.UnmarshalBase64(value, &conf); err != nil {
			return
		}
		s.log.Trace().Str("stream", s.stream).Msgf("[hksv] stream id=%x cmd=%d", conf.Control.SessionID, conf.Control.Command)

		switch conf.Control.Command {
		case camera.SessionCommandEnd:
			_ = s.liveStream.StopStream(conf.Control.SessionID, s)
		case camera.SessionCommandStart:
			_ = s.liveStream.StartStream(s.stream, &conf, s)
		}

	case camera.TypeSetupDataStreamTransport:
		var req camera.SetupDataStreamTransportRequest
		if err := tlv8.UnmarshalBase64(value, &req); err != nil {
			s.log.Error().Err(err).Str("stream", s.stream).Msg("[hksv] parse ch131 failed")
			return
		}

		s.log.Debug().Str("stream", s.stream).Uint8("cmd", req.SessionCommandType).
			Uint8("transport", req.TransportType).Msg("[hksv] DataStream setup")

		if req.SessionCommandType != 0 {
			s.log.Debug().Str("stream", s.stream).Msg("[hksv] DataStream close request")
			if s.hksvSession != nil {
				s.hksvSession.Close()
			}
			return
		}

		accessoryKeySalt := core.RandString(32, 0)
		combinedSalt := req.ControllerKeySalt + accessoryKeySalt

		ln, err := net.ListenTCP("tcp", nil)
		if err != nil {
			s.log.Error().Err(err).Str("stream", s.stream).Msg("[hksv] listen failed")
			return
		}
		port := ln.Addr().(*net.TCPAddr).Port

		resp := camera.SetupDataStreamTransportResponse{
			Status:           0,
			AccessoryKeySalt: accessoryKeySalt,
		}
		resp.TransportTypeSessionParameters.TCPListeningPort = uint16(port)

		v, err := tlv8.MarshalBase64(resp)
		if err != nil {
			ln.Close()
			return
		}
		char.Value = v

		s.log.Debug().Str("stream", s.stream).Int("port", port).Msg("[hksv] listening for HDS")

		hapConn := conn.(*hap.Conn)
		go s.acceptHDS(hapConn, ln, combinedSalt)

	case camera.TypeSelectedCameraRecordingConfiguration:
		s.log.Debug().Str("stream", s.stream).Str("motion", s.motionMode).Msg("[hksv] selected recording config")
		char.Value = value

		switch s.motionMode {
		case "continuous":
			go s.startContinuousMotion()
		case "detect":
			go s.startMotionDetector()
		}

	default:
		char.Value = value
	}
}

func (s *Server) GetImage(conn net.Conn, width, height int) []byte {
	s.log.Trace().Str("stream", s.stream).Msgf("[hksv] get image width=%d height=%d", width, height)

	if s.snapshots == nil {
		return nil
	}

	b, err := s.snapshots.GetSnapshot(s.stream, width, height)
	if err != nil {
		s.log.Error().Err(err).Msg("[hksv] snapshot failed")
		return nil
	}
	return b
}

// SetMotionDetected triggers or clears the motion detected characteristic.
func (s *Server) SetMotionDetected(detected bool) {
	if s.accessory == nil {
		s.log.Warn().Str("stream", s.stream).Msg("[hksv] SetMotionDetected: accessory is nil")
		return
	}
	char := s.accessory.GetCharacter("22") // MotionDetected
	if char == nil {
		s.log.Warn().Str("stream", s.stream).Msg("[hksv] SetMotionDetected: char 22 (MotionDetected) not found")
		return
	}
	char.Value = detected
	listeners := char.ListenerCount()
	err := char.NotifyListeners(nil)
	s.log.Debug().Str("stream", s.stream).Bool("motion", detected).
		Int("listeners", listeners).Err(err).Msg("[hksv] motion")
}

// MotionDetected returns the current motion detected state.
func (s *Server) MotionDetected() bool {
	if s.accessory == nil {
		return false
	}
	char := s.accessory.GetCharacter("22") // MotionDetected
	if char == nil {
		return false
	}
	v, _ := char.Value.(bool)
	return v
}

// TriggerDoorbell triggers a doorbell press event.
func (s *Server) TriggerDoorbell() {
	if s.accessory == nil {
		return
	}
	char := s.accessory.GetCharacter("73") // ProgrammableSwitchEvent
	if char == nil {
		return
	}
	char.Value = 0 // SINGLE_PRESS
	_ = char.NotifyListeners(nil)
	s.log.Debug().Str("stream", s.stream).Msg("[hksv] doorbell")
}

// acceptHDS opens a TCP listener for the HDS DataStream connection from the Home Hub
func (s *Server) acceptHDS(hapConn *hap.Conn, ln net.Listener, salt string) {
	defer ln.Close()

	if tcpLn, ok := ln.(*net.TCPListener); ok {
		_ = tcpLn.SetDeadline(time.Now().Add(30 * time.Second))
	}

	rawConn, err := ln.Accept()
	if err != nil {
		s.log.Error().Err(err).Str("stream", s.stream).Msg("[hksv] accept failed")
		return
	}
	defer rawConn.Close()

	hdsConn, err := hds.NewConn(rawConn, hapConn.SharedKey, salt, false)
	if err != nil {
		s.log.Error().Err(err).Str("stream", s.stream).Msg("[hksv] hds conn failed")
		return
	}

	s.AddConn(hdsConn)
	defer s.DelConn(hdsConn)

	session := newHKSVSession(s, hapConn, hdsConn)

	s.mu.Lock()
	s.hksvSession = session
	s.mu.Unlock()

	defer func() {
		s.mu.Lock()
		if s.hksvSession == session {
			s.hksvSession = nil
		}
		s.mu.Unlock()
		session.Close()
	}()

	s.log.Debug().Str("stream", s.stream).Msg("[hksv] session started")

	if err := session.Run(); err != nil {
		s.log.Debug().Err(err).Str("stream", s.stream).Msg("[hksv] session ended")
	}
}

// prepareHKSVConsumer pre-starts a consumer and adds it to the stream.
func (s *Server) prepareHKSVConsumer() {
	consumer := NewHKSVConsumer(s.log)

	if err := s.streams.AddConsumer(s.stream, consumer); err != nil {
		s.log.Debug().Err(err).Str("stream", s.stream).Msg("[hksv] prepare consumer failed")
		return
	}

	s.log.Debug().Str("stream", s.stream).Msg("[hksv] consumer prepared")

	s.mu.Lock()
	if s.preparedConsumer != nil {
		old := s.preparedConsumer
		s.preparedConsumer = nil
		s.mu.Unlock()
		s.streams.RemoveConsumer(s.stream, old)
		_ = old.Stop()
		s.mu.Lock()
	}
	s.preparedConsumer = consumer
	s.mu.Unlock()

	// Keep alive until used or timeout (60 seconds)
	select {
	case <-consumer.Done():
		// consumer was stopped (used or server closed)
	case <-time.After(60 * time.Second):
		s.mu.Lock()
		if s.preparedConsumer == consumer {
			s.preparedConsumer = nil
			s.mu.Unlock()
			s.streams.RemoveConsumer(s.stream, consumer)
			_ = consumer.Stop()
			s.log.Debug().Str("stream", s.stream).Msg("[hksv] prepared consumer expired")
		} else {
			s.mu.Unlock()
		}
	}
}

func (s *Server) takePreparedConsumer() *HKSVConsumer {
	s.mu.Lock()
	defer s.mu.Unlock()
	consumer := s.preparedConsumer
	s.preparedConsumer = nil
	return consumer
}

func (s *Server) startMotionDetector() {
	s.mu.Lock()
	if s.motionDetector != nil {
		s.mu.Unlock()
		return
	}
	det := NewMotionDetector(s.motionThreshold, s.SetMotionDetected, s.log)
	s.motionDetector = det
	s.mu.Unlock()

	s.AddConn(det)

	if err := s.streams.AddConsumer(s.stream, det); err != nil {
		s.log.Error().Err(err).Str("stream", s.stream).Msg("[hksv] motion detector add consumer failed")
		s.DelConn(det)
		s.mu.Lock()
		s.motionDetector = nil
		s.mu.Unlock()
		return
	}

	s.log.Debug().Str("stream", s.stream).Msg("[hksv] motion detector started")

	_, _ = det.WriteTo(nil) // blocks until Stop()

	s.streams.RemoveConsumer(s.stream, det)
	s.DelConn(det)

	s.mu.Lock()
	if s.motionDetector == det {
		s.motionDetector = nil
	}
	s.mu.Unlock()

	s.log.Debug().Str("stream", s.stream).Msg("[hksv] motion detector stopped")
}

func (s *Server) stopMotionDetector() {
	s.mu.Lock()
	det := s.motionDetector
	s.mu.Unlock()
	if det != nil {
		_ = det.Stop()
	}
}

func (s *Server) startContinuousMotion() {
	s.mu.Lock()
	if s.continuousMotion {
		s.mu.Unlock()
		return
	}
	s.continuousMotion = true
	s.mu.Unlock()

	s.log.Debug().Str("stream", s.stream).Msg("[hksv] continuous motion started")

	// delay to allow Home Hub to subscribe to events
	time.Sleep(5 * time.Second)

	s.SetMotionDetected(true)

	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()

	for range ticker.C {
		if s.accessory == nil {
			return
		}
		s.SetMotionDetected(true)
	}
}

// isClosedConnErr checks if the error is a "use of closed network connection" error.
// This happens when the remote side (e.g., iPhone) closes the TCP connection.
func isClosedConnErr(err error) bool {
	if err == nil {
		return false
	}
	return strings.Contains(err.Error(), "use of closed network connection")
}
