package main

import (
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"text/tabwriter"
)

// clientKeys holds a freshly generated key triple.
type clientKeys struct {
	priv string
	pub  string
	psk  string
}

// generateClientKeys produces the client's private/public/preshared keys.
func generateClientKeys() clientKeys {
	priv, err := output("awg", "genkey")
	if err != nil {
		die("awg genkey failed: %v", err)
	}
	pub, err := outputWithInput(priv, "awg", "pubkey")
	if err != nil {
		die("awg pubkey failed: %v", err)
	}
	psk, err := output("awg", "genpsk")
	if err != nil {
		die("awg genpsk failed: %v", err)
	}
	return clientKeys{priv: priv, pub: pub, psk: psk}
}

// createClientConfig writes <name>.conf plus a scannable <name>.png QR code.
func createClientConfig(c *Config, id int64, name, ip string, keys clientKeys) {
	filename := filepath.Join(clientDir, name+".conf")

	content := fmt.Sprintf(`# Client: %s
# ID: %d

[Interface]
PrivateKey = %s
Address = %s/32
DNS = %s
MTU = %s

Jc = %s
Jmin = %s
Jmax = %s
S1 = %s
S2 = %s
H1 = %s
H2 = %s
H3 = %s
H4 = %s

[Peer]
PublicKey = %s
PresharedKey = %s
AllowedIPs = 0.0.0.0/0
Endpoint = %s:%s
PersistentKeepalive = 25
`, name, id,
		keys.priv, ip, c.DNS, c.MTU,
		c.Jc, c.Jmin, c.Jmax, c.S1, c.S2, c.H1, c.H2, c.H3, c.H4,
		c.ServerPub, keys.psk,
		c.PublicIP, c.Port)

	if err := os.WriteFile(filename, []byte(content), 0600); err != nil {
		die("Failed to write client config: %v", err)
	}

	pngPath := filepath.Join(clientDir, name+".png")
	if err := run("qrencode", "-s", "8", "-o", pngPath, "-r", filename); err != nil {
		die("qrencode failed: %v", err)
	}
	// The QR code encodes the full .conf, private key included; qrencode
	// creates it with the process umask (typically 0644). Lock it down to
	// match the .conf it was generated from.
	if err := os.Chmod(pngPath, 0600); err != nil {
		die("Failed to secure QR code permissions: %v", err)
	}

	info("Client config created: %s.conf", name)
}

// createClient generates keys, config, QR, registers the client and hot-adds
// the peer to the running interface.
func createClient(c *Config, osInfo *OSInfo, args []string) {
	if len(args) == 0 || args[0] == "" {
		die("Client name required")
	}
	if len(args) > 1 {
		die("Unexpected argument: %s", args[1])
	}

	name := sanitize(args[0])
	if name == "" {
		die("Sanitized name is empty")
	}

	clients := loadRegistry()
	id := getNextID(clients)
	lastIP := getLastIP(clients)
	nextIP := incrementIP(lastIP, c.Network)

	keys := generateClientKeys()
	createClientConfig(c, id, name, nextIP, keys)

	clients = append(clients, Client{
		ID:         id,
		Name:       name,
		IP:         nextIP,
		PublicKey:  keys.pub,
		PrivateKey: keys.priv,
		PSKKey:     keys.psk,
		IsEnabled:  "ACTIVE",
		CreatedAt:  nowUnix(),
	})
	saveRegistry(clients)

	info("Client registered: id=%d name=%s ip=%s", id, name, nextIP)

	// Activate peer on the running server immediately — no restart needed.
	confAppendPeer(c.Interface, name, keys.pub, keys.psk, nextIP)
	awgPeerAdd(c.Interface, keys.pub, keys.psk, nextIP)
}

// listClients prints an aligned table of all registered clients.
func listClients() {
	clients := loadRegistry()
	if len(clients) == 0 {
		fmt.Println("No clients registered")
		return
	}
	w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
	fmt.Fprintln(w, "ID\tNAME\tIP\tSTATUS")
	for _, cl := range clients {
		fmt.Fprintf(w, "%d\t%s\t%s\t%s\n", cl.ID, cl.Name, cl.IP, cl.IsEnabled)
	}
	w.Flush()
}

// parseID validates and parses a numeric client ID argument.
func parseID(args []string) int64 {
	if len(args) == 0 {
		die("Invalid ID")
	}
	id, err := strconv.ParseInt(args[0], 10, 64)
	if err != nil || id < 0 {
		die("Invalid ID")
	}
	return id
}

func disableClient(c *Config, osInfo *OSInfo, args []string) {
	id := parseID(args)
	clients := loadRegistry()
	cl := findClient(clients, id)
	if cl == nil {
		die("Client with ID %d not found", id)
	}
	if cl.IsEnabled != "ACTIVE" {
		die("Client %d (%s) is already DISABLED", id, cl.Name)
	}

	pubkey := cl.PublicKey
	name := cl.Name
	cl.IsEnabled = "DISABLED"
	saveRegistry(clients)

	awgPeerRemove(c.Interface, pubkey)
	confRebuild(c, osInfo, clients)

	info("Client %d (%s): ACTIVE → DISABLED", id, name)
}

func enableClient(c *Config, osInfo *OSInfo, args []string) {
	id := parseID(args)
	clients := loadRegistry()
	cl := findClient(clients, id)
	if cl == nil {
		die("Client with ID %d not found", id)
	}
	if cl.IsEnabled != "DISABLED" {
		die("Client %d (%s) is already ACTIVE", id, cl.Name)
	}

	pubkey, psk, ip, name := cl.PublicKey, cl.PSKKey, cl.IP, cl.Name
	cl.IsEnabled = "ACTIVE"
	saveRegistry(clients)

	awgPeerAdd(c.Interface, pubkey, psk, ip)
	confRebuild(c, osInfo, clients)

	info("Client %d (%s): DISABLED → ACTIVE", id, name)
}

// setClientComment updates a client's free-form note in the registry. It touches
// neither the interface config nor the running peers — the comment is metadata
// only — so no rebuild/restart is needed.
func setClientComment(id int64, comment string) *Client {
	clients := loadRegistry()
	cl := findClient(clients, id)
	if cl == nil {
		die("Client with ID %d not found", id)
	}
	cl.Comment = comment
	saveRegistry(clients)
	info("Client %d (%s): comment updated", id, cl.Name)
	return cl
}

func deleteClient(c *Config, osInfo *OSInfo, args []string) {
	id := parseID(args)
	clients := loadRegistry()
	cl := findClient(clients, id)
	if cl == nil {
		die("Client with ID %d not found", id)
	}

	name := cl.Name
	pubkey := cl.PublicKey

	remaining := make([]Client, 0, len(clients))
	for _, x := range clients {
		if x.ID != id {
			remaining = append(remaining, x)
		}
	}
	saveRegistry(remaining)

	os.Remove(filepath.Join(clientDir, name+".conf"))
	os.Remove(filepath.Join(clientDir, name+".png"))

	awgPeerRemove(c.Interface, pubkey)
	confRebuild(c, osInfo, remaining)
	deleteStats(pubkey) // drop the peer's accumulated traffic record

	info("Client %d (%s) removed", id, name)
}
