// Command labbridge is the standalone LabBridge Agent: a clinic-side service
// that relays analyzer messages (ASTM E1394 over TCP/serial) to the cloud
// LabCore API with offline-safe SQLite queuing.
//
// Subcommands (via the kardianos/service convention):
//
//	labbridge install     # register as a Windows Service
//	labbridge uninstall
//	labbridge start | stop | restart | status
//	labbridge run         # foreground run (default if no subcommand)
//
// All subcommands honor the -config flag; default is config.yaml next to the
// binary.
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"sync"
	"time"

	"github.com/kardianos/service"

	"tps.local/labbridge-agent/internal/config"
	"tps.local/labbridge-agent/internal/listener"
	"tps.local/labbridge-agent/internal/queue"
	syncpkg "tps.local/labbridge-agent/internal/sync"
)

func main() {
	configPath := flag.String("config", defaultConfigPath(), "Path to config.yaml")
	flag.Parse()

	svcConfig := &service.Config{
		Name:        "LabBridgeAgent",
		DisplayName: "LabBridge Agent",
		Description: "Relays clinic analyzer messages to the cloud LabCore API.",
		Arguments:   []string{"-config", *configPath},
	}

	prg := &program{configPath: *configPath}
	s, err := service.New(prg, svcConfig)
	if err != nil {
		log.Fatalf("service.New: %v", err)
	}
	prg.service = s

	// Subcommand shortcut: `labbridge install|uninstall|start|stop|restart|status|run`.
	if flag.NArg() > 0 {
		cmd := flag.Arg(0)
		if cmd == "run" {
			runForeground(prg)
			return
		}
		if err := service.Control(s, cmd); err != nil {
			fmt.Fprintln(os.Stderr, err)
			fmt.Fprintln(os.Stderr, "Valid actions: install, uninstall, start, stop, restart, status, run")
			os.Exit(1)
		}
		fmt.Printf("%s OK\n", cmd)
		return
	}

	// No subcommand → let the service framework decide (running as a service
	// vs interactive); when interactive, fall through to run.
	if err := s.Run(); err != nil {
		log.Fatalf("service.Run: %v", err)
	}
}

// program implements service.Interface (Start/Stop) and owns the agent's
// running goroutines.
type program struct {
	configPath string
	service    service.Service

	cancel context.CancelFunc
	wg     sync.WaitGroup
}

func (p *program) Start(_ service.Service) error {
	ctx, cancel := context.WithCancel(context.Background())
	p.cancel = cancel
	p.wg.Add(1)
	go func() {
		defer p.wg.Done()
		if err := runAgent(ctx, p.configPath); err != nil {
			log.Printf("agent exited with error: %v", err)
		}
	}()
	return nil
}

func (p *program) Stop(_ service.Service) error {
	if p.cancel != nil {
		p.cancel()
	}
	p.wg.Wait()
	return nil
}

// runForeground is what `labbridge run` invokes — same body, no service wrap.
func runForeground(p *program) {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	if err := runAgent(ctx, p.configPath); err != nil {
		log.Fatalf("agent: %v", err)
	}
}

// runAgent loads config, opens the queue, registers with the cloud, starts
// listeners + sync workers, and blocks until ctx is cancelled.
func runAgent(ctx context.Context, configPath string) error {
	cfg, err := config.Load(configPath)
	if err != nil {
		return fmt.Errorf("config: %w", err)
	}
	log.Printf("LabBridge Agent %s starting — cloud=%s data=%s", cfg.AgentVersion, cfg.CloudURL, cfg.DataDir)

	store, err := queue.Open(cfg.QueuePath())
	if err != nil {
		return fmt.Errorf("queue: %w", err)
	}
	defer store.Close()

	client := syncpkg.New(cfg.CloudURL, cfg.APIToken, cfg.AgentVersion)

	reg, err := client.Register()
	if err != nil {
		log.Printf("register failed (will rely on cached state): %v", err)
	} else {
		log.Printf("registered as agent #%d %q — %d device(s)", reg.Agent.ID, reg.Agent.Name, len(reg.Devices))
	}

	worker := syncpkg.NewWorker(store, client, time.Duration(cfg.RetryInterval)*time.Second)
	hb := syncpkg.NewHeartbeat(store, client, time.Duration(cfg.HeartbeatInterval)*time.Second)

	mgr := listener.NewManager(store, worker)
	if reg != nil {
		mgr.Start(ctx, reg.Devices)
	}
	defer mgr.Stop()

	go worker.Run(ctx)
	go hb.Run(ctx)

	<-ctx.Done()
	log.Printf("shutting down")
	return nil
}

func defaultConfigPath() string {
	exe, err := os.Executable()
	if err != nil {
		return "config.yaml"
	}
	return filepath.Join(filepath.Dir(exe), "config.yaml")
}
