// Copyright 2026. Licensed under Apache-2.0. See LICENSE.
// Generated by CLI Printing Press (https://github.com/mvanhorn/cli-printing-press). DO NOT EDIT.

package device

import (
	"context"
	"encoding/hex"
	"fmt"
	"time"

	"ble-temperature-sensor-pp-cli/internal/cliutil"
)

// LiveTransport drives the device over a real BLE connection (when the binary is
// built with -tags ble_live). It implements Transport, so the same commands that
// replay evidence by default actuate hardware under --live. Without the build
// tag, every operation returns ErrLiveUnavailable.
type LiveTransport struct {
	address string
	timeout time.Duration
}

func NewLiveTransport(address string, timeout time.Duration) *LiveTransport {
	if timeout <= 0 {
		timeout = 20 * time.Second
	}
	return &LiveTransport{address: address, timeout: timeout}
}

// opContext bounds an operation by the configured timeout, curtailed under the
// live-dogfood matrix so a single connect+op fits the runner's per-command cap.
func (t *LiveTransport) opContext(parent context.Context) (context.Context, context.CancelFunc) {
	timeout := t.timeout
	if cliutil.IsDogfoodEnv() && timeout > 5*time.Second {
		timeout = 5 * time.Second
	}
	return context.WithTimeout(parent, timeout)
}

func (t *LiveTransport) Status(ctx context.Context) (StatusSnapshot, error) {
	if cliutil.IsVerifyEnv() {
		return StatusSnapshot{Device: DisplayName, Transport: "verify-live-noop", SessionMode: SessionMode, ObservedAt: nowUTC(), Telemetry: map[string]any{}}, nil
	}
	ctx, cancel := t.opContext(ctx)
	defer cancel()
	telemetry := map[string]any{}
	err := t.withLink(ctx, func(l Link) error {
		// Notify-only telemetry cannot be GATT-read; an operator snapshot captures
		// one notification frame that carries every field. When set, decode all
		// fields from that frame instead of reading each characteristic.
		var snapshot []byte
		var snapshotHex string
		if telemetrySnapshot != nil {
			s, snapErr := telemetrySnapshot(ctx, l)
			if snapErr != nil {
				return snapErr
			}
			snapshot, snapshotHex = s, hex.EncodeToString(s)
		}
		for _, field := range StatusFields {
			entry := map[string]any{"source_characteristic_uuid": field.SourceCharacteristicUUID}
			raw, rawHex := snapshot, snapshotHex
			if telemetrySnapshot == nil {
				r, readErr := l.Read(field.SourceCharacteristicUUID)
				if readErr != nil {
					// Readable characteristics surface a value directly; notify-only
					// vendor telemetry needs a snapshot, so report why it is empty.
					entry["error"] = readErr.Error()
					telemetry[field.Name] = entry
					continue
				}
				raw, rawHex = r, hex.EncodeToString(r)
			}
			entry["raw_hex"] = rawHex
			if codec != nil {
				if value, decErr := codec.DecodeTelemetry(field, raw); decErr != nil {
					entry["decode_error"] = decErr.Error()
				} else {
					entry["value"] = value
				}
			}
			telemetry[field.Name] = entry
		}
		return nil
	})
	if err != nil {
		return StatusSnapshot{}, err
	}
	return StatusSnapshot{Device: DisplayName, Transport: "live", SessionMode: SessionMode, ObservedAt: nowUTC(), Telemetry: telemetry}, nil
}

// liveResult builds the CommandResult fields common to every live outcome,
// leaving the caller to set the distinguishing dry-run/verify fields.
func liveResult(command CommandDefinition, transport string) CommandResult {
	return CommandResult{
		Command:            command.Name,
		Transport:          transport,
		CharacteristicUUID: command.CharacteristicUUID,
		PayloadHex:         command.PayloadHex,
		Safety:             command.Safety,
		ValidationStatus:   command.ValidationStatus,
	}
}

func (t *LiveTransport) ExecuteCommand(ctx context.Context, command CommandDefinition, args []string, dryRun bool) (CommandResult, error) {
	if cliutil.IsVerifyEnv() {
		result := liveResult(command, "verify-live-noop")
		result.DryRun, result.VerifyNoop, result.Reason = true, true, "verify_short_circuit"
		return result, nil
	}
	payload, err := encodeCommandPayload(command, args)
	if err != nil {
		return CommandResult{}, err
	}
	result := liveResult(command, "live")
	result.PayloadHex = hex.EncodeToString(payload) // the bytes actually sent (may be codec-encoded)
	if dryRun {
		result.DryRun = true
		return result, nil
	}
	ctx, cancel := t.opContext(ctx)
	defer cancel()
	if err := t.withLink(ctx, func(l Link) error {
		return l.Write(command.CharacteristicUUID, payload)
	}); err != nil {
		return CommandResult{}, err
	}
	return result, nil
}

// encodeCommandPayload builds the bytes to write: the codec (when registered)
// owns encoding and may use args for parameterized commands; otherwise the
// captured static payload is used. A parameterized command with no codec is a
// configuration error.
func encodeCommandPayload(command CommandDefinition, args []string) ([]byte, error) {
	if codec != nil {
		return codec.EncodeCommand(command, args)
	}
	if len(args) > 0 {
		return nil, fmt.Errorf("command %q takes arguments but no DeviceCodec is registered to encode them", command.Name)
	}
	payload, err := hex.DecodeString(command.PayloadHex)
	if err != nil {
		return nil, fmt.Errorf("decode payload %q: %w", command.PayloadHex, err)
	}
	return payload, nil
}

// Scan discovers nearby devices that expose the device's BLE service(s).
func (t *LiveTransport) Scan(ctx context.Context) ([]Advert, error) {
	if cliutil.IsVerifyEnv() {
		return nil, nil
	}
	be, err := bleBackendFactory()
	if err != nil {
		return nil, err
	}
	ctx, cancel := t.opContext(ctx)
	defer cancel()
	adverts, err := be.Scan(ctx, ServiceUUIDs)
	if err != nil {
		return nil, err
	}
	sortByRSSI(adverts)
	return adverts, nil
}

// withLink dials the device and runs fn against the connection, then closes it.
func (t *LiveTransport) withLink(ctx context.Context, fn func(Link) error) error {
	l, err := Dial(ctx, t.address, t.timeout)
	if err != nil {
		return err
	}
	defer func() { _ = l.Close() }()
	return fn(l)
}

// Dial opens a live BLE connection to the device, scanning by ServiceUUIDs when
// address is empty. The caller owns the returned Link and must Close it. Needs a
// binary built with -tags ble_live. Hand-authored commands use Dial + Link for
// parameterized or stateful control beyond the generated command surface; gate
// them on cliutil.IsVerifyEnv()/IsDogfoodEnv() as the skill describes (Dial
// itself refuses under verify as a backstop).
func Dial(ctx context.Context, address string, timeout time.Duration) (Link, error) {
	if cliutil.IsVerifyEnv() {
		return nil, ErrVerifyMode
	}
	if _, hasDeadline := ctx.Deadline(); !hasDeadline && timeout > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, timeout)
		defer cancel() // scopes scan+connect; the established link does not use ctx
	}
	be, err := bleBackendFactory()
	if err != nil {
		return nil, err
	}
	if address == "" {
		adverts, err := be.Scan(ctx, ServiceUUIDs)
		if err != nil {
			return nil, err
		}
		sortByRSSI(adverts)
		if len(adverts) == 0 {
			return nil, ErrDeviceNotFound
		}
		address = adverts[0].Address
	}
	return be.Connect(ctx, address, ServiceUUIDs)
}

func nowUTC() string { return time.Now().UTC().Format(time.RFC3339) }

func sortByRSSI(adverts []Advert) {
	// Strongest (highest, closest to 0) RSSI first. Small slices; insertion sort
	// avoids importing sort for one call.
	for i := 1; i < len(adverts); i++ {
		for j := i; j > 0 && adverts[j].RSSI > adverts[j-1].RSSI; j-- {
			adverts[j], adverts[j-1] = adverts[j-1], adverts[j]
		}
	}
}
