// Command catools provides CA (Channel Access) command-line utilities using
// the pure-Go goca library.
//
// Usage:
//
//	catools get     <pv> [pv...]     Get current value(s)
//	catools put     <pv> <value>     Write a value
//	catools monitor <pv> [pv...]     Stream live updates (Ctrl-C to stop)
//	catools info    <pv> [pv...]     Print metadata (type, units, limits)
//
// Environment:
//
//	EPICS_CA_ADDR_LIST       — space-separated CA server addresses
//	EPICS_CA_AUTO_ADDR_LIST  — set to "NO" to disable broadcast search
//	UOPI_CA_DEBUG            — set to "1" for CA protocol debug logging
package main

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

	ca "github.com/uopi/goca"
	"github.com/uopi/goca/proto"
)

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

Usage:
  catools get     <pv> [pv...]     Get current value(s)
  catools put     <pv> <value>     Write a value
  catools monitor <pv> [pv...]     Stream live updates (Ctrl-C to stop)
  catools info    <pv> [pv...]     Print metadata (type, units, limits)

Environment:
  EPICS_CA_ADDR_LIST       Space-separated CA server addresses
  EPICS_CA_AUTO_ADDR_LIST  Set to NO to disable broadcast search
  UOPI_CA_DEBUG            Set to 1 for CA protocol debug logging`)
	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()

	cfg := ca.ConfigFromEnv()
	cli, err := ca.NewClient(ctx, cfg)
	if err != nil {
		fatalf("connect: %v", err)
	}

	switch cmd {
	case "get":
		if len(args) == 0 {
			fatalf("get: no PV names specified")
		}
		runGet(ctx, cli, args)
	case "put":
		if len(args) < 2 {
			fatalf("put: usage: catools put <pv> <value>")
		}
		runPut(ctx, cli, args[0], args[1])
	case "info":
		if len(args) == 0 {
			fatalf("info: no PV names specified")
		}
		runInfo(ctx, cli, args)
	case "monitor":
		if len(args) == 0 {
			fatalf("monitor: no PV names specified")
		}
		runMonitor(ctx, cli, args)
	default:
		fatalf("unknown command %q — use get, put, monitor, or info", cmd)
	}
}

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

func runGet(ctx context.Context, cli *ca.Client, pvs []string) {
	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

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

// -------------------------------------------------------------------------- //
// put                                                                         //
// -------------------------------------------------------------------------- //

func runPut(ctx context.Context, cli *ca.Client, pv, valueStr string) {
	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	// Get metadata to determine the right type.
	ci, err := cli.GetCtrl(tctx, pv)
	if err != nil {
		fatalf("%s: get metadata: %v", pv, err)
	}

	var value any
	switch ci.DBFType {
	case proto.DBFString:
		value = valueStr
	case proto.DBFEnum:
		if n, err := strconv.ParseInt(valueStr, 10, 32); err == nil {
			value = int32(n)
		} else if ci.Enum != nil {
			for i, s := range ci.Enum.Strings {
				if strings.EqualFold(s, valueStr) {
					value = int32(i)
					break
				}
			}
		}
		if value == nil {
			fatalf("%s: unknown enum value %q", pv, valueStr)
		}
	default:
		if n, err := strconv.ParseFloat(valueStr, 64); err == nil {
			value = n
		} else if n, err := strconv.ParseInt(valueStr, 10, 64); err == nil {
			value = int64(n)
		} else {
			value = valueStr
		}
	}

	if err := cli.Put(tctx, pv, value); err != nil {
		fatalf("%s: put: %v", pv, err)
	}

	tv, err := cli.Get(tctx, pv)
	if err != nil {
		fmt.Printf("%-30s %s (write sent, readback failed: %v)\n", pv, valueStr, err)
		return
	}
	fmt.Printf("%-30s %s\n", pv, formatValue(tv))
}

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

func runInfo(ctx context.Context, cli *ca.Client, pvs []string) {
	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	for _, pv := range pvs {
		ci, err := cli.GetCtrl(tctx, pv)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s: error: %v\n", pv, err)
			continue
		}
		printInfo(pv, ci)
		fmt.Println()
	}
}

func printInfo(pv string, ci ca.CtrlInfo) {
	fmt.Printf("Name       : %s\n", pv)
	fmt.Printf("DBF type   : %s (%d)\n", dbfTypeName(ci.DBFType), ci.DBFType)
	fmt.Printf("Count      : %d\n", ci.Count)
	writable := "No"
	if ci.Access&proto.AccessWrite != 0 {
		writable = "Yes"
	}
	fmt.Printf("Writable   : %s\n", writable)

	switch {
	case ci.Double != nil:
		d := ci.Double
		fmt.Printf("Units      : %s\n", d.Units)
		fmt.Printf("Disp range : [%g, %g]\n", d.LowerDispLimit, d.UpperDispLimit)
		fmt.Printf("Ctrl range : [%g, %g]\n", d.LowerCtrlLimit, d.UpperCtrlLimit)
		fmt.Printf("Alarm LOLO : %g  HIHI: %g\n", d.LowerAlarmLimit, d.UpperAlarmLimit)
		fmt.Printf("Warn  LOW  : %g  HIGH: %g\n", d.LowerWarnLimit, d.UpperWarnLimit)
	case ci.Long != nil:
		l := ci.Long
		fmt.Printf("Units      : %s\n", l.Units)
		fmt.Printf("Disp range : [%d, %d]\n", l.LowerDispLimit, l.UpperDispLimit)
		fmt.Printf("Ctrl range : [%d, %d]\n", l.LowerCtrlLimit, l.UpperCtrlLimit)
		fmt.Printf("Alarm LOLO : %d  HIHI: %d\n", l.LowerAlarmLimit, l.UpperAlarmLimit)
		fmt.Printf("Warn  LOW  : %d  HIGH: %d\n", l.LowerWarnLimit, l.UpperWarnLimit)
	case ci.Enum != nil:
		fmt.Printf("Enum states: %d\n", len(ci.Enum.Strings))
		for i, s := range ci.Enum.Strings {
			fmt.Printf("  [%d] %s\n", i, s)
		}
	case ci.Str != nil:
		fmt.Printf("Type       : string\n")
	}
}

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

func runMonitor(ctx context.Context, cli *ca.Client, pvs []string) {
	type entry struct {
		pv  string
		ch  chan proto.TimeValue
		can context.CancelFunc
	}

	entries := make([]*entry, 0, len(pvs))
	tctx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	for _, pv := range pvs {
		ch := make(chan proto.TimeValue, 32)
		cancelFn, err := cli.Subscribe(tctx, pv, ch)
		if err != nil {
			fmt.Fprintf(os.Stderr, "%s: subscribe error: %v\n", pv, err)
			continue
		}
		entries = append(entries, &entry{pv: pv, ch: ch, can: cancelFn})
		fmt.Printf("Monitoring %s — press Ctrl-C to stop\n", pv)
	}

	if len(entries) == 0 {
		return
	}

	for _, e := range entries {
		go func(e *entry) {
			for {
				select {
				case tv, ok := <-e.ch:
					if !ok {
						return
					}
					ts := tv.Timestamp.Local().Format("15:04:05.000")
					fmt.Printf("%s  %-30s  %s\n", ts, e.pv, formatValue(tv))
				case <-ctx.Done():
					return
				}
			}
		}(e)
	}

	<-ctx.Done()
	for _, e := range entries {
		e.can()
	}
}

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

func formatValue(tv proto.TimeValue) string {
	var val string
	switch {
	case tv.Str != "":
		val = tv.Str
	case len(tv.Doubles) > 1:
		parts := make([]string, len(tv.Doubles))
		for i, v := range tv.Doubles {
			parts[i] = strconv.FormatFloat(v, 'g', -1, 64)
		}
		val = "[" + strings.Join(parts, " ") + "]"
	default:
		val = strconv.FormatFloat(tv.Double, 'g', -1, 64)
	}

	severity := ""
	switch tv.Severity {
	case proto.SeverityMinor:
		severity = " MINOR"
	case proto.SeverityMajor:
		severity = " MAJOR"
	case proto.SeverityInvalid:
		severity = " INVALID"
	}

	ts := tv.Timestamp.Local().Format("2006-01-02 15:04:05.000")
	return fmt.Sprintf("%s%s  [%s]", val, severity, ts)
}

func dbfTypeName(t int) string {
	switch t {
	case proto.DBFString:
		return "DBF_STRING"
	case proto.DBFShort:
		return "DBF_SHORT"
	case proto.DBFFloat:
		return "DBF_FLOAT"
	case proto.DBFEnum:
		return "DBF_ENUM"
	case proto.DBFChar:
		return "DBF_CHAR"
	case proto.DBFLong:
		return "DBF_LONG"
	case proto.DBFDouble:
		return "DBF_DOUBLE"
	default:
		return "DBF_UNKNOWN"
	}
}

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