// Command pvtools provides PV Access command-line utilities using
// the pure-Go gopva library.
//
// Usage:
//
//	pvtools get     <pv> [pv...]     Get current value(s) via PVA
//	pvtools put     <pv> <value>     Write a value via PVA
//	pvtools monitor <pv> [pv...]     Stream live updates (Ctrl-C to stop)
//	pvtools info    <pv> [pv...]     Print PVA structure info
//
// Environment:
//
//	EPICS_PVA_ADDR_LIST  — space-separated PVA server addresses
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"strconv"
	"strings"
	"syscall"
	"time"

	pva "github.com/uopi/gopva"
	"github.com/uopi/gopva/pvdata"
)

func usage() {
	fmt.Fprintln(os.Stderr, `pvtools — EPICS PV Access command-line tool

Usage:
  pvtools get     <pv> [pv...]     Get current value(s)
  pvtools put     <pv> <value>     Write a value
  pvtools monitor <pv> [pv...]     Stream live updates (Ctrl-C to stop)
  pvtools info    <pv> [pv...]     Print structure info

Environment:
  EPICS_PVA_ADDR_LIST  Space-separated PVA server addresses`)
	os.Exit(1)
}

func main() {
	if len(os.Args) < 2 {
		usage()
	}

	cmd := os.Args[1]
	args := os.Args[2:]

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	switch cmd {
	case "get":
		if len(args) == 0 {
			fatalf("get: no PV names specified")
		}
		runGet(ctx, args)
	case "put":
		if len(args) < 2 {
			fatalf("put: usage: pvtools put <pv> <value>")
		}
		fatalf("put: PVA write not yet implemented")
	case "monitor":
		if len(args) == 0 {
			fatalf("monitor: no PV names specified")
		}
		runMonitor(ctx, args)
	case "info":
		if len(args) == 0 {
			fatalf("info: no PV names specified")
		}
		runInfo(ctx, args)
	default:
		fatalf("unknown command %q — use get, put, monitor, or info", cmd)
	}
}

// -------------------------------------------------------------------------- //
// get                                                                         //
// -------------------------------------------------------------------------- //

func runGet(ctx context.Context, pvs []string) {
	cl, err := pva.NewClient()
	if err != nil {
		fatalf("connect: %v", err)
	}
	defer cl.Close()

	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	for _, name := range pvs {
		sv, err := cl.Get(tctx, name)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s: error: %v\n", name, err)
			continue
		}
		fmt.Printf("%-30s %s\n", name, formatValue(sv))
	}
}

// -------------------------------------------------------------------------- //
// monitor                                                                     //
// -------------------------------------------------------------------------- //

func runMonitor(ctx context.Context, pvs []string) {
	cl, err := pva.NewClient()
	if err != nil {
		fatalf("connect: %v", err)
	}
	defer cl.Close()

	for _, name := range pvs {
		ch := cl.Monitor(ctx, name)
		go func(pvName string, ch <-chan pva.MonitorEvent) {
			for evt := range ch {
				if evt.Err != nil {
					fmt.Fprintf(os.Stderr, "%s: %v\n", pvName, evt.Err)
					return
				}
				ts := time.Now().Local().Format("15:04:05.000")
				fmt.Printf("%s  %-30s  %s\n", ts, pvName, formatValue(evt.Value))
			}
		}(name, ch)
		fmt.Printf("Monitoring %s (PVA) — press Ctrl-C to stop\n", name)
	}
	<-ctx.Done()
}

// -------------------------------------------------------------------------- //
// info                                                                        //
// -------------------------------------------------------------------------- //

func runInfo(ctx context.Context, pvs []string) {
	cl, err := pva.NewClient()
	if err != nil {
		fatalf("connect: %v", err)
	}
	defer cl.Close()

	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	for _, name := range pvs {
		sv, err := cl.Get(tctx, name)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s: error: %v\n", name, err)
			continue
		}
		printInfo(name, sv)
		fmt.Println()
	}
}

func printInfo(name string, sv pvdata.StructValue) {
	fmt.Printf("Name   : %s\n", name)
	fmt.Printf("Fields : %d\n", len(sv.Fields))
	printStruct(sv, "  ")
}

func printStruct(sv pvdata.StructValue, indent string) {
	for _, f := range sv.Fields {
		switch v := f.Value.(type) {
		case pvdata.StructValue:
			fmt.Printf("%s%s (struct):\n", indent, f.Name)
			printStruct(v, indent+"  ")
		default:
			fmt.Printf("%s%s = %v\n", indent, f.Name, v)
		}
	}
}

// -------------------------------------------------------------------------- //
// Formatting helpers                                                          //
// -------------------------------------------------------------------------- //

func formatValue(sv pvdata.StructValue) string {
	if len(sv.Fields) == 0 {
		return "<empty>"
	}
	val := fieldByName(sv, "value")
	alarm := ""
	if a := structByName(sv, "alarm"); a != nil {
		if sev := fieldByName(*a, "severity"); sev != nil {
			if s, ok := sev.(int32); ok && s != 0 {
				alarm = fmt.Sprintf(" (alarm sev=%d)", s)
			}
		}
	}
	ts := ""
	if t := structByName(sv, "timeStamp"); t != nil {
		sec := int64(0)
		nsec := int32(0)
		if v := fieldByName(*t, "secondsPastEpoch"); v != nil {
			sec, _ = v.(int64)
		}
		if v := fieldByName(*t, "nanoseconds"); v != nil {
			nsec, _ = v.(int32)
		}
		if sec != 0 {
			const epicsEpoch = 631152000
			ut := time.Unix(sec+epicsEpoch, int64(nsec)).Local()
			ts = "  [" + ut.Format("2006-01-02 15:04:05.000") + "]"
		}
	}
	if val == nil {
		val = sv.Fields[0].Value
	}
	return fmt.Sprintf("%v%s%s", formatScalar(val), alarm, ts)
}

func formatScalar(v any) string {
	switch x := v.(type) {
	case float64:
		return strconv.FormatFloat(x, 'g', -1, 64)
	case float32:
		return strconv.FormatFloat(float64(x), 'g', -1, 32)
	case string:
		return x
	case []string:
		return strings.Join(x, " ")
	default:
		return fmt.Sprintf("%v", v)
	}
}

func fieldByName(sv pvdata.StructValue, name string) interface{} {
	for _, f := range sv.Fields {
		if f.Name == name {
			return f.Value
		}
	}
	return nil
}

func structByName(sv pvdata.StructValue, name string) *pvdata.StructValue {
	for _, f := range sv.Fields {
		if f.Name == name {
			if s, ok := f.Value.(pvdata.StructValue); ok {
				return &s
			}
		}
	}
	return nil
}

func fatalf(format string, args ...any) {
	fmt.Fprintf(os.Stderr, "pvtools: "+format+"\n", args...)
	os.Exit(1)
}
