//go:build ble_live

// Copyright 2026. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.
//
// Live BLE backend: binds the device-neutral bleBackend/Link seam to a real
// adapter via tinygo.org/x/bluetooth. Compiled only with -tags ble_live and
// requires CGO (CoreBluetooth on macOS, BlueZ/D-Bus on Linux, WinRT on Windows).
// The default build uses ble_stub.go and links no BLE stack.

package device

import (
	"context"
	"fmt"
	"strings"
	"sync"
	"time"

	tinyble "tinygo.org/x/bluetooth"
)

// liveCompiled is true: the real BLE backend is linked in.
const liveCompiled = true

func newBLEBackend() (bleBackend, error) {
	adapter := tinyble.DefaultAdapter
	if err := adapter.Enable(); err != nil {
		return nil, fmt.Errorf("enable BLE adapter (check OS Bluetooth permission for your terminal): %w", err)
	}
	return &liveBackend{adapter: adapter}, nil
}

type liveBackend struct {
	adapter *tinyble.Adapter
}

func (b *liveBackend) Scan(ctx context.Context, serviceUUIDs []string) ([]Advert, error) {
	filters, err := parseUUIDs(serviceUUIDs)
	if err != nil {
		return nil, err
	}
	if err := ctx.Err(); err != nil {
		return nil, err // don't start a scan with an already-expired context
	}
	// Cap the scan window so it never consumes a long caller deadline (e.g. a
	// stream's whole duration) and starve the connect that follows discovery.
	dur := 8 * time.Second
	if dl, ok := ctx.Deadline(); ok {
		if d := time.Until(dl); d > 0 && d < dur {
			dur = d
		}
	}
	var (
		mu   sync.Mutex
		seen = map[string]Advert{}
		done = make(chan struct{})
		once sync.Once
	)
	finish := func() {
		once.Do(func() {
			_ = b.adapter.StopScan()
			close(done)
		})
	}
	timer := time.NewTimer(dur)
	defer timer.Stop()
	go func() {
		select {
		case <-ctx.Done():
		case <-timer.C:
		}
		finish()
	}()
	scanErr := b.adapter.Scan(func(_ *tinyble.Adapter, result tinyble.ScanResult) {
		if !matchesAnyService(result, filters) {
			return
		}
		mu.Lock()
		seen[result.Address.String()] = Advert{
			Address:      result.Address.String(),
			Name:         result.LocalName(),
			RSSI:         int(result.RSSI),
			ServiceUUIDs: serviceUUIDs,
		}
		mu.Unlock()
		finish() // Scan locates one device to connect to next; stop on first match so the connect is not delayed
	})
	finish()
	<-done
	if scanErr != nil {
		return nil, fmt.Errorf("scan: %w", scanErr)
	}
	mu.Lock()
	defer mu.Unlock()
	out := make([]Advert, 0, len(seen))
	for _, a := range seen {
		out = append(out, a)
	}
	return out, nil
}

func (b *liveBackend) Connect(ctx context.Context, address string, serviceUUIDs []string) (Link, error) {
	timeout := 20 * time.Second
	if dl, ok := ctx.Deadline(); ok {
		if d := time.Until(dl); d > 0 {
			timeout = d
		}
	}
	var addr tinyble.Address
	addr.Set(address)
	dev, err := b.adapter.Connect(addr, tinyble.ConnectionParams{
		ConnectionTimeout: tinyble.NewDuration(timeout),
	})
	if err != nil {
		return nil, fmt.Errorf("connect %s: %w", address, err)
	}
	filters, err := parseUUIDs(serviceUUIDs)
	if err != nil {
		_ = dev.Disconnect()
		return nil, err
	}
	services, err := dev.DiscoverServices(filters)
	if err != nil {
		_ = dev.Disconnect()
		return nil, fmt.Errorf("discover services: %w", err)
	}
	chars := map[string]*tinyble.DeviceCharacteristic{}
	for i := range services {
		// nil discovers every characteristic of the service.
		discovered, err := services[i].DiscoverCharacteristics(nil)
		if err != nil {
			_ = dev.Disconnect()
			return nil, fmt.Errorf("discover characteristics: %w", err)
		}
		for j := range discovered {
			chars[normalizeUUID(discovered[j].UUID().String())] = &discovered[j]
		}
	}
	return &liveLink{dev: dev, chars: chars}, nil
}

type liveLink struct {
	dev   tinyble.Device
	chars map[string]*tinyble.DeviceCharacteristic
}

func (l *liveLink) char(characteristicUUID string) (*tinyble.DeviceCharacteristic, error) {
	c, ok := l.chars[normalizeUUID(characteristicUUID)]
	if !ok {
		return nil, fmt.Errorf("characteristic %s not found on device", characteristicUUID)
	}
	return c, nil
}

func (l *liveLink) Write(characteristicUUID string, payload []byte) error {
	c, err := l.char(characteristicUUID)
	if err != nil {
		return err
	}
	// Prefer an acknowledged write so the command is confirmed by the device
	// before we return: an unacknowledged write can be dropped if the caller
	// closes the link or issues the next command immediately (a control command
	// like stop is then silently lost). Fall back to write-without-response for
	// characteristics that only permit the latter.
	if _, err := c.Write(payload); err == nil {
		return nil
	}
	_, err = c.WriteWithoutResponse(payload)
	return err
}

func (l *liveLink) Read(characteristicUUID string) ([]byte, error) {
	c, err := l.char(characteristicUUID)
	if err != nil {
		return nil, err
	}
	buf := make([]byte, 512)
	n, err := c.Read(buf)
	if err != nil {
		return nil, err
	}
	return buf[:n], nil
}

func (l *liveLink) Subscribe(characteristicUUID string, handler func([]byte)) error {
	c, err := l.char(characteristicUUID)
	if err != nil {
		return err
	}
	return c.EnableNotifications(handler)
}

func (l *liveLink) Close() error {
	return l.dev.Disconnect()
}

func parseUUIDs(uuids []string) ([]tinyble.UUID, error) {
	out := make([]tinyble.UUID, 0, len(uuids))
	for _, u := range uuids {
		parsed, err := tinyble.ParseUUID(u)
		if err != nil {
			return nil, fmt.Errorf("parse uuid %q: %w", u, err)
		}
		out = append(out, parsed)
	}
	return out, nil
}

func matchesAnyService(result tinyble.ScanResult, filters []tinyble.UUID) bool {
	if len(filters) == 0 {
		return true
	}
	for _, f := range filters {
		if result.HasServiceUUID(f) {
			return true
		}
	}
	return false
}

func normalizeUUID(u string) string {
	return strings.ToLower(strings.TrimSpace(u))
}
