package main

import (
	"encoding/json"
	"os"
	"sort"
	"time"
)

// Client is one entry in the JSON registry. The field tags reproduce the exact
// object shape the bash/jq version wrote, so registries are interchangeable.
type Client struct {
	ID         int64  `json:"id"`
	Name       string `json:"name"`
	IP         string `json:"ip"`
	PublicKey  string `json:"public_key"`
	PrivateKey string `json:"private_key"`
	PSKKey     string `json:"psk_key"`
	IsEnabled  string `json:"is_enabled"`
	CreatedAt  int64  `json:"created_at"`
	// Comment is a free-form note attached to the profile via the web UI. It is
	// registry-only (never written into the .conf) and omitted when empty so
	// registries stay byte-compatible with the bash/jq version until a note is set.
	Comment string `json:"comment,omitempty"`
}

// initStorage creates the data/client directories and an empty registry,
// validating any existing registry JSON (bash: jq empty). Both directories
// hold secret material (the registry's private_key field, client .conf/.png
// files) so they are created owner-only.
func initStorage() {
	if err := os.MkdirAll(dataDir, 0700); err != nil {
		die("Failed to create data dir: %v", err)
	}
	if err := os.MkdirAll(clientDir, 0700); err != nil {
		die("Failed to create client dir: %v", err)
	}
	if _, err := os.Stat(registryFile); os.IsNotExist(err) {
		if err := os.WriteFile(registryFile, []byte("[]"), 0644); err != nil {
			die("Failed to initialise registry: %v", err)
		}
	}
	// Validate JSON (equivalent to `jq empty`).
	loadRegistry()
}

// loadRegistry reads and parses the registry, aborting on invalid JSON.
func loadRegistry() []Client {
	data, err := os.ReadFile(registryFile)
	if err != nil {
		die("Failed to read registry: %v", err)
	}
	var clients []Client
	if err := json.Unmarshal(data, &clients); err != nil {
		die("Registry JSON is invalid")
	}
	return clients
}

// saveRegistry writes the registry atomically (temp file + rename), matching
// the bash mktemp/mv pattern, then re-validates it.
func saveRegistry(clients []Client) {
	data, err := json.MarshalIndent(clients, "", "  ")
	if err != nil {
		die("Failed to update registry: %v", err)
	}
	tmp, err := os.CreateTemp(dataDir, "registry-*.tmp")
	if err != nil {
		die("Failed to update registry: %v", err)
	}
	tmpName := tmp.Name()
	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		os.Remove(tmpName)
		die("Failed to update registry: %v", err)
	}
	tmp.Close()
	if err := os.Rename(tmpName, registryFile); err != nil {
		os.Remove(tmpName)
		die("Failed to update registry: %v", err)
	}
	// Re-validate (bash: jq empty after write).
	loadRegistry()
}

// getNextID returns max(id)+1, or 1 for an empty registry.
func getNextID(clients []Client) int64 {
	if len(clients) == 0 {
		return 1
	}
	var max int64
	for _, c := range clients {
		if c.ID > max {
			max = c.ID
		}
	}
	return max + 1
}

// getLastIP returns the IP of the highest-id client (empty registry → "").
func getLastIP(clients []Client) string {
	if len(clients) == 0 {
		return ""
	}
	sorted := append([]Client(nil), clients...)
	sort.Slice(sorted, func(i, j int) bool { return sorted[i].ID < sorted[j].ID })
	return sorted[len(sorted)-1].IP
}

// findClient returns a pointer to the client with the given id, or nil.
func findClient(clients []Client, id int64) *Client {
	for i := range clients {
		if clients[i].ID == id {
			return &clients[i]
		}
	}
	return nil
}

// nowUnix returns the current Unix timestamp (bash: now|floor).
func nowUnix() int64 {
	return time.Now().Unix()
}
