// panelctl is the operator-facing CLI. It talks to the Controller's Panel
// gRPC service to inspect connected agents and drive instance lifecycle.
//
// Usage:
//
//	panelctl [flags] agents
//	panelctl [flags] create <agent-id> <instance-id> <module-id>
//	panelctl [flags] start  <agent-id> <instance-id>
//	panelctl [flags] stop   <agent-id> <instance-id> [grace-seconds]
//
// Flags:
//
//	--controller host:port   Controller gRPC address (default localhost:8443)
package main

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"flag"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/insecure"

	panelv1 "github.com/dbledeez/panel/proto/panel/v1"
)

func usage() {
	fmt.Fprintln(os.Stderr, `panelctl — operator CLI for panel.

Usage:
  panelctl [flags] agents
  panelctl [flags] instances [agent-id]
  panelctl [flags] create <agent-id> <instance-id> <module-id>
  panelctl [flags] start  <agent-id> <instance-id>
  panelctl [flags] stop   <agent-id> <instance-id> [grace-seconds]
  panelctl [flags] watch   [instance-id]
  panelctl [flags] rcon    <agent-id> <instance-id> <command...>
  panelctl [flags] delete  <agent-id> <instance-id> [--purge]
  panelctl [flags] update  <agent-id> <instance-id> [provider-id]
  panelctl [flags] modules
  panelctl [flags] backup  list    <instance-id>
  panelctl [flags] backup  create  <agent-id> <instance-id> [description]
  panelctl [flags] backup  restore <instance-id> <backup-id>
  panelctl admin <subcommand>      Out-of-band repair (HTTP). See: panelctl admin

Flags:
  --controller host:port   Controller gRPC address (default localhost:8443)
  --cert-dir DIR           Use mTLS with agent certs from DIR (ca.crt + agent.crt + agent.key).
                           Defaults to /home/refuge/panel/data/certs if it exists.
                           Without certs, dials plaintext (only works against --insecure controllers).`)
}

func main() {
	// "admin" is a parallel subcommand group that uses HTTP, not gRPC, and
	// has its own --http / --admin-token / --data-dir flags. Dispatch it
	// before flag.Parse so its FlagSet handles its own args without colliding
	// with the gRPC --controller flag below.
	if len(os.Args) >= 2 && os.Args[1] == "admin" {
		cmdAdmin(os.Args[2:])
		return
	}

	controllerAddr := flag.String("controller", "localhost:8443", "controller gRPC address")
	certDir := flag.String("cert-dir", defaultCertDir(), "directory holding ca.crt + agent.crt + agent.key for mTLS (empty disables)")
	flag.Usage = usage
	flag.Parse()

	args := flag.Args()
	if len(args) < 1 {
		usage()
		os.Exit(2)
	}

	// Most subcommands need a short deadline; watch overrides.
	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()
	if len(args) > 0 && (args[0] == "watch" || args[0] == "w" || args[0] == "tail") {
		cancel()
		ctx = context.Background()
	}

	dialCreds, err := buildCreds(*certDir, *controllerAddr)
	if err != nil {
		die("tls: %v", err)
	}
	conn, err := grpc.NewClient(*controllerAddr, grpc.WithTransportCredentials(dialCreds))
	if err != nil {
		die("grpc client: %v", err)
	}
	defer conn.Close()

	client := panelv1.NewPanelClient(conn)

	switch args[0] {
	case "agents", "ls", "list":
		cmdAgents(ctx, client)
	case "instances", "insts", "i":
		agentFilter := ""
		if len(args) >= 2 {
			agentFilter = args[1]
		}
		cmdInstances(ctx, client, agentFilter)
	case "watch", "w", "tail":
		// watch has its own long-lived ctx (not timeout-bound)
		watchCtx, watchCancel := context.WithCancel(context.Background())
		defer watchCancel()
		instanceFilter := ""
		if len(args) >= 2 {
			instanceFilter = args[1]
		}
		cmdWatch(watchCtx, client, instanceFilter)
	case "create":
		if len(args) < 4 {
			die("usage: panelctl create <agent-id> <instance-id> <module-id>")
		}
		cmdCreate(ctx, client, args[1], args[2], args[3])
	case "start":
		if len(args) < 3 {
			die("usage: panelctl start <agent-id> <instance-id>")
		}
		cmdStart(ctx, client, args[1], args[2])
	case "stop":
		if len(args) < 3 {
			die("usage: panelctl stop <agent-id> <instance-id> [grace-seconds]")
		}
		grace := uint32(30)
		if len(args) >= 4 {
			g, err := strconv.ParseUint(args[3], 10, 32)
			if err != nil {
				die("invalid grace-seconds %q: %v", args[3], err)
			}
			grace = uint32(g)
		}
		cmdStop(ctx, client, args[1], args[2], grace)
	case "rcon", "r":
		if len(args) < 4 {
			die("usage: panelctl rcon <agent-id> <instance-id> <command...>")
		}
		cmd := args[3]
		for _, s := range args[4:] {
			cmd += " " + s
		}
		cmdRCON(ctx, client, args[1], args[2], cmd)
	case "delete", "rm", "del":
		if len(args) < 3 {
			die("usage: panelctl delete <agent-id> <instance-id> [--purge]")
		}
		purge := false
		for _, a := range args[3:] {
			if a == "--purge" {
				purge = true
			}
		}
		cmdDelete(ctx, client, args[1], args[2], purge)
	case "update", "upd":
		if len(args) < 3 {
			die("usage: panelctl update <agent-id> <instance-id> [provider-id]")
		}
		providerID := ""
		if len(args) >= 4 {
			providerID = args[3]
		}
		cmdUpdate(ctx, client, args[1], args[2], providerID)
	case "modules", "mods":
		cmdModules(ctx, *controllerAddr)
	case "backup", "bkp":
		if len(args) < 3 {
			die("usage: panelctl backup <list|create|restore> ...")
		}
		cmdBackup(ctx, client, *controllerAddr, args[2:])
	default:
		usage()
		os.Exit(2)
	}
}

func cmdAgents(ctx context.Context, c panelv1.PanelClient) {
	resp, err := c.ListAgents(ctx, &panelv1.ListAgentsRequest{})
	if err != nil {
		die("list agents: %v", err)
	}
	if len(resp.Agents) == 0 {
		fmt.Println("(no agents connected)")
		return
	}
	fmt.Printf("%-24s  %-10s  %-12s  %-10s  %s\n", "AGENT_ID", "VERSION", "OS/ARCH", "HOSTNAME", "CONNECTED")
	for _, a := range resp.Agents {
		conn := a.ConnectedAt.AsTime().Format(time.RFC3339)
		fmt.Printf("%-24s  %-10s  %-12s  %-10s  %s\n",
			a.AgentId, a.Version, a.HostOs+"/"+a.HostArch, a.Hostname, conn)
	}
}

func cmdInstances(ctx context.Context, c panelv1.PanelClient, agentFilter string) {
	resp, err := c.ListInstances(ctx, &panelv1.ListInstancesRequest{AgentId: agentFilter})
	if err != nil {
		die("list instances: %v", err)
	}
	if len(resp.Instances) == 0 {
		fmt.Println("(no instances)")
		return
	}
	fmt.Printf("%-18s  %-18s  %-14s  %-10s  %-18s  %s\n",
		"INSTANCE_ID", "AGENT_ID", "MODULE", "STATUS", "UPDATED", "DETAIL")
	for _, i := range resp.Instances {
		module := i.ModuleId
		if i.ModuleVersion != "" {
			module = module + "@" + i.ModuleVersion
		}
		updated := i.UpdatedAt.AsTime().Format("01-02 15:04:05")
		fmt.Printf("%-18s  %-18s  %-14s  %-10s  %-18s  %s\n",
			i.InstanceId, i.AgentId, module, shortStatus(i.Status), updated, i.Detail)
	}
}

func shortStatus(s panelv1.InstanceStatus) string {
	switch s {
	case panelv1.InstanceStatus_INSTANCE_STATUS_STOPPED:
		return "stopped"
	case panelv1.InstanceStatus_INSTANCE_STATUS_STARTING:
		return "starting"
	case panelv1.InstanceStatus_INSTANCE_STATUS_RUNNING:
		return "running"
	case panelv1.InstanceStatus_INSTANCE_STATUS_STOPPING:
		return "stopping"
	case panelv1.InstanceStatus_INSTANCE_STATUS_CRASHED:
		return "crashed"
	case panelv1.InstanceStatus_INSTANCE_STATUS_UPDATING:
		return "updating"
	default:
		return "unknown"
	}
}

func cmdWatch(ctx context.Context, c panelv1.PanelClient, instanceFilter string) {
	stream, err := c.StreamEvents(ctx, &panelv1.StreamEventsRequest{InstanceId: instanceFilter})
	if err != nil {
		die("stream events: %v", err)
	}
	if instanceFilter == "" {
		fmt.Fprintln(os.Stderr, "watching all events; Ctrl-C to stop")
	} else {
		fmt.Fprintf(os.Stderr, "watching instance %s; Ctrl-C to stop\n", instanceFilter)
	}
	for {
		ev, err := stream.Recv()
		if err != nil {
			fmt.Fprintf(os.Stderr, "stream ended: %v\n", err)
			return
		}
		renderEvent(ev)
	}
}

func renderEvent(ev *panelv1.Event) {
	at := ev.At.AsTime().Format("15:04:05")
	switch p := ev.Payload.(type) {
	case *panelv1.Event_InstanceState:
		fmt.Printf("%s  STATE  %-14s  %-10s  %s\n",
			at, p.InstanceState.InstanceId, shortStatus(p.InstanceState.Status), p.InstanceState.Detail)
	case *panelv1.Event_AppState:
		fmt.Printf("%s  APP    %-14s  players=%d/%d  uptime=%ds\n",
			at, p.AppState.InstanceId, p.AppState.PlayersOnline, p.AppState.PlayersMax, p.AppState.UptimeSeconds)
	case *panelv1.Event_Player:
		fmt.Printf("%s  %-6s %-14s  %s  %s\n",
			at, shortKind(p.Player.Kind), p.Player.InstanceId, p.Player.PlayerName, p.Player.Detail)
	case *panelv1.Event_Log:
		fmt.Printf("%s  LOG    %-14s  %s\n",
			at, p.Log.InstanceId, p.Log.Line)
	default:
		fmt.Printf("%s  ?      %T\n", at, ev.Payload)
	}
}

func shortKind(k panelv1.PlayerEvent_Kind) string {
	switch k {
	case panelv1.PlayerEvent_KIND_JOIN:
		return "JOIN"
	case panelv1.PlayerEvent_KIND_LEAVE:
		return "LEAVE"
	case panelv1.PlayerEvent_KIND_CHAT:
		return "CHAT"
	case panelv1.PlayerEvent_KIND_DEATH:
		return "DEATH"
	case panelv1.PlayerEvent_KIND_CUSTOM:
		return "CUST"
	default:
		return "?"
	}
}

func cmdCreate(ctx context.Context, c panelv1.PanelClient, agentID, instanceID, moduleID string) {
	resp, err := c.CreateInstance(ctx, &panelv1.CreateInstanceRequest{
		AgentId:    agentID,
		InstanceId: instanceID,
		ModuleId:   moduleID,
		RunMode:    panelv1.RunMode_RUN_MODE_DOCKER,
	})
	if err != nil {
		die("create: %v", err)
	}
	fmt.Printf("create OK instance=%s correlation=%s\n", resp.InstanceId, resp.CorrelationId)
}

func cmdStart(ctx context.Context, c panelv1.PanelClient, agentID, instanceID string) {
	resp, err := c.StartInstance(ctx, &panelv1.StartInstanceRequest{
		AgentId:    agentID,
		InstanceId: instanceID,
	})
	if err != nil {
		die("start: %v", err)
	}
	fmt.Printf("start OK instance=%s correlation=%s\n", instanceID, resp.CorrelationId)
}

func cmdRCON(ctx context.Context, c panelv1.PanelClient, agentID, instanceID, command string) {
	resp, err := c.ExecRCON(ctx, &panelv1.ExecRCONRequest{
		AgentId:    agentID,
		InstanceId: instanceID,
		Command:    command,
		TimeoutMs:  10000,
	})
	if err != nil {
		die("rcon: %v", err)
	}
	if resp.Error != "" {
		fmt.Fprintf(os.Stderr, "error: %s\n", resp.Error)
	}
	if resp.Output != "" {
		fmt.Print(resp.Output)
		if !strings.HasSuffix(resp.Output, "\n") {
			fmt.Println()
		}
	}
}

func cmdStop(ctx context.Context, c panelv1.PanelClient, agentID, instanceID string, grace uint32) {
	resp, err := c.StopInstance(ctx, &panelv1.StopInstanceRequest{
		AgentId:      agentID,
		InstanceId:   instanceID,
		GraceSeconds: grace,
	})
	if err != nil {
		die("stop: %v", err)
	}
	fmt.Printf("stop OK instance=%s correlation=%s\n", instanceID, resp.CorrelationId)
}

func cmdDelete(ctx context.Context, c panelv1.PanelClient, agentID, instanceID string, purge bool) {
	resp, err := c.DeleteInstance(ctx, &panelv1.DeleteInstanceRequest{
		AgentId:      agentID,
		InstanceId:   instanceID,
		PurgeVolumes: purge,
	})
	if err != nil {
		die("delete: %v", err)
	}
	fmt.Printf("delete queued instance=%s purge=%t correlation=%s\n", instanceID, purge, resp.CorrelationId)
}

func cmdUpdate(ctx context.Context, c panelv1.PanelClient, agentID, instanceID, providerID string) {
	resp, err := c.UpdateInstance(ctx, &panelv1.UpdateInstanceRequest{
		AgentId:    agentID,
		InstanceId: instanceID,
		ProviderId: providerID,
	})
	if err != nil {
		die("update: %v", err)
	}
	fmt.Printf("update accepted=%t provider=%s kind=%s correlation=%s error=%s\n",
		resp.Accepted, resp.ProviderId, resp.ProviderKind, resp.CorrelationId, resp.Error)
	fmt.Fprintln(os.Stderr, "watch progress with: panelctl watch "+instanceID)
}

// cmdModules is REST because the modules endpoint isn't a Panel RPC.
func cmdModules(ctx context.Context, controllerAddr string) {
	base := "http://" + strings.TrimPrefix(strings.Split(controllerAddr, ":")[0], "http://") + ":8080"
	_ = ctx
	_ = base
	fmt.Println("modules: hit the HTTP dashboard's /api/modules or visit the UI")
	fmt.Println("(gRPC ListModules RPC not wired yet — mirroring only via HTTP for now)")
}

func cmdBackup(ctx context.Context, c panelv1.PanelClient, controllerAddr string, args []string) {
	if len(args) < 1 {
		die("usage: panelctl backup <list|create|restore> ...")
	}
	switch args[0] {
	case "create":
		if len(args) < 3 {
			die("usage: panelctl backup create <agent-id> <instance-id> [description]")
		}
		desc := ""
		if len(args) >= 4 {
			desc = strings.Join(args[3:], " ")
		}
		resp, err := c.CreateBackup(ctx, &panelv1.CreateBackupRequest{
			AgentId:     args[1],
			InstanceId:  args[2],
			Description: desc,
		})
		if err != nil {
			die("backup: %v", err)
		}
		fmt.Printf("backup %s  %d bytes  path=%s\n", resp.BackupId, resp.SizeBytes, resp.Path)
	case "restore":
		if len(args) < 3 {
			die("usage: panelctl backup restore <instance-id> <backup-id>")
		}
		resp, err := c.RestoreBackup(ctx, &panelv1.RestoreBackupRequest{
			InstanceId: args[1],
			BackupId:   args[2],
		})
		if err != nil {
			die("restore: %v", err)
		}
		fmt.Printf("restore ok — %d bytes restored\n", resp.BytesRestored)
	case "list":
		fmt.Println("backup list: hit /api/instances/<id>/backups via HTTP (no gRPC ListBackups RPC yet)")
	default:
		die("unknown backup subcommand %q", args[0])
	}
	_ = controllerAddr
}

func die(format string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, "panelctl: "+format+"\n", args...)
	os.Exit(1)
}

// defaultCertDir returns /home/refuge/panel/data/certs when it exists (the
// standard prod path), otherwise empty string. This lets `panelctl agents`
// just work when run on princess without operators having to specify a flag.
func defaultCertDir() string {
	const prodPath = "/home/refuge/panel/data/certs"
	if _, err := os.Stat(filepath.Join(prodPath, "agent.crt")); err == nil {
		return prodPath
	}
	return ""
}

// buildCreds returns mTLS credentials when certDir contains the expected
// agent material; falls back to insecure (plaintext) when certDir is empty.
// The mTLS path mirrors the agent's loadClientTLS so panelctl can talk to
// the same prod gRPC endpoint without a special-cased "operator cert".
func buildCreds(certDir, controllerAddr string) (credentials.TransportCredentials, error) {
	if certDir == "" {
		return insecure.NewCredentials(), nil
	}
	caPath := filepath.Join(certDir, "ca.crt")
	certPath := filepath.Join(certDir, "agent.crt")
	keyPath := filepath.Join(certDir, "agent.key")
	caPEM, err := os.ReadFile(caPath)
	if err != nil {
		return nil, fmt.Errorf("read ca: %w", err)
	}
	pool := x509.NewCertPool()
	if !pool.AppendCertsFromPEM(caPEM) {
		return nil, fmt.Errorf("bad CA PEM")
	}
	cert, err := tls.LoadX509KeyPair(certPath, keyPath)
	if err != nil {
		return nil, fmt.Errorf("load cert+key: %w", err)
	}
	serverName := controllerAddr
	if i := strings.LastIndex(serverName, ":"); i > 0 {
		serverName = serverName[:i]
	}
	return credentials.NewTLS(&tls.Config{
		Certificates: []tls.Certificate{cert},
		RootCAs:      pool,
		ServerName:   serverName,
		MinVersion:   tls.VersionTLS12,
	}), nil
}
