package main

import (
	"errors"
	"flag"
	"fmt"
	"os"
	"strings"
	"text/tabwriter"

	"github.com/enmanuel/unibus/pkg/membership"
)

// runUserCLI implements `membershipd user <add|list|revoke> ...`, the local
// administration surface for the bus user allowlist. It opens the SQLite store
// directly (no network, no auth): it is meant to run on the bus host, where
// shell access already implies full control. This is the seam that seeds the
// first admin, breaking the chicken-egg of "you need an admin to add an admin".
//
// The function never returns: it exits the process with a non-zero status on
// error so it composes cleanly in shell scripts and systemd ExecStartPre hooks.
func runUserCLI(args []string) {
	if len(args) == 0 {
		userUsage()
		os.Exit(2)
	}
	sub, rest := args[0], args[1:]
	switch sub {
	case "add":
		userAdd(rest)
	case "list":
		userList(rest)
	case "revoke":
		userRevoke(rest)
	case "-h", "--help", "help":
		userUsage()
		os.Exit(0)
	default:
		fmt.Fprintf(os.Stderr, "membershipd user: unknown subcommand %q\n\n", sub)
		userUsage()
		os.Exit(2)
	}
}

func userUsage() {
	fmt.Fprint(os.Stderr, `usage: membershipd user <command> [flags]

commands:
  add      Register a bus user from their Ed25519 signing public key
  list     List all registered users
  revoke   Revoke a user (denies access on both planes immediately)

store backends (--store):
  sqlite   local SQLite database (default; seeds the first admin offline)
  kv       the RUNNING cluster's replicated JetStream KV allowlist, via the
           privileged internal connection — add users with the cluster live,
           no stop-seed-restart needed (run over loopback/SSH on a node)

examples:
  membershipd user add --handle alice --sign-pub <64-hex> --role admin
  membershipd user add --store kv --handle bob --sign-pub <64-hex> --role member
  membershipd user list --store kv
  membershipd user revoke <64-hex>

common flags:
  --db <path>   SQLite database path (--store sqlite; default ./local_files/unibus.db)

--store kv flags (defaults assume an on-node invocation):
  --nats-url <url>           cluster NATS (default nats://127.0.0.1:4250)
  --internal-id-file <path>  persisted internal service identity (default /opt/unibus/secrets/internal.id)
  --ca <path>                CA cert pinning the data-plane TLS (default /opt/unibus/tls/ca.crt)
  --kv-replicas <n>          KV replication factor, match the cluster (default 3)
`)
}

const defaultDBPath = "./local_files/unibus.db"

// openStore opens the membership store at path, exiting on failure. Migrations
// (including 002_users.sql) are applied by membership.Open, so a fresh database
// gets the users table on first use of the CLI.
func openStore(path string) membership.Store {
	store, err := membership.Open(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "membershipd user: open store %q: %v\n", path, err)
		os.Exit(1)
	}
	return store
}

// validateSignPubHex ensures the key is exactly a 32-byte Ed25519 public key in
// hex (64 hex chars). Catching this here turns a silent "authorized nobody" into
// an explicit error at seed time. It delegates to membership.ValidateSignPubHex
// so the CLI and the HTTP user-management handlers share one rule.
func validateSignPubHex(signPub string) error {
	return membership.ValidateSignPubHex(signPub)
}

// kvFlags holds the connection flags shared by the --store kv path of the user
// subcommands. registerKVFlags wires them onto a flag set so add and list expose
// an identical interface.
type kvFlags struct {
	store      *string
	natsURL    *string
	internalID *string
	ca         *string
	replicas   *int
}

func registerKVFlags(fs *flag.FlagSet) kvFlags {
	return kvFlags{
		store:      fs.String("store", "sqlite", "user store backend: sqlite (local DB) | kv (the live cluster's replicated allowlist)"),
		natsURL:    fs.String("nats-url", defaultClusterNatsURL, "cluster NATS url for --store kv"),
		internalID: fs.String("internal-id-file", defaultInternalIDFile, "persisted internal service identity for --store kv"),
		ca:         fs.String("ca", defaultClusterCAFile, "CA cert pinning TLS on the --store kv NATS connection"),
		replicas:   fs.Int("kv-replicas", 3, "KV replication factor for --store kv (match the cluster)"),
	}
}

// resolveStore returns the membership store for the chosen backend plus a cleanup
// func. For --store kv it opens the privileged connection to the live cluster; for
// sqlite it opens the local file. It exits the process with a clear message on any
// failure (a dead NATS, a missing identity file), so a broken --store kv add fails
// loudly instead of silently — Error case of the GAP A DoD. The returned *kvConn
// is non-nil only for the kv backend (so the caller can report replication).
func resolveStore(cmd string, kf kvFlags, dbPath string) (membership.Store, *kvConn, func()) {
	switch *kf.store {
	case "sqlite":
		store := openStore(dbPath)
		return store, nil, func() { store.Close() }
	case "kv":
		kv, err := connectKVStore(*kf.natsURL, *kf.internalID, *kf.ca, *kf.replicas)
		if err != nil {
			fmt.Fprintf(os.Stderr, "membershipd %s: --store kv: %v\n", cmd, err)
			os.Exit(1)
		}
		return kv.store, kv, kv.Close
	default:
		fmt.Fprintf(os.Stderr, "membershipd %s: --store must be \"sqlite\" or \"kv\", got %q\n", cmd, *kf.store)
		os.Exit(2)
		return nil, nil, func() {}
	}
}

func userAdd(args []string) {
	fs := flag.NewFlagSet("user add", flag.ExitOnError)
	handle := fs.String("handle", "", "human-readable user name (required)")
	signPub := fs.String("sign-pub", "", "Ed25519 signing public key in hex (required)")
	role := fs.String("role", membership.RoleMember, "role: admin or member")
	dbPath := fs.String("db", defaultDBPath, "SQLite database path")
	kf := registerKVFlags(fs)
	_ = fs.Parse(args)

	if *handle == "" || *signPub == "" {
		fmt.Fprintln(os.Stderr, "membershipd user add: --handle and --sign-pub are required")
		os.Exit(2)
	}
	if err := validateSignPubHex(*signPub); err != nil {
		fmt.Fprintf(os.Stderr, "membershipd user add: %v\n", err)
		os.Exit(2)
	}

	store, kv, closeStore := resolveStore("user add", kf, *dbPath)
	defer closeStore()

	if err := store.AddUser(*signPub, *handle, *role); err != nil {
		if errors.Is(err, membership.ErrUserExists) {
			// Idempotency contract (GAP A): re-adding the same key is an EXPLICIT,
			// non-destructive error — the existing row is left untouched (no silent
			// upsert that could flip a role or clobber status, which would corrupt the
			// allowlist). To replace a user, `user revoke <key>` then add again.
			fmt.Fprintf(os.Stderr, "membershipd user add: user %s already registered (unchanged); revoke it first to replace\n", *signPub)
			os.Exit(1)
		}
		fmt.Fprintf(os.Stderr, "membershipd user add: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("added user %q (%s) role=%s\n", *handle, *signPub, *role)
	if kv != nil {
		reportKVReplication(kv.js)
	}
}

func userList(args []string) {
	fs := flag.NewFlagSet("user list", flag.ExitOnError)
	dbPath := fs.String("db", defaultDBPath, "SQLite database path")
	kf := registerKVFlags(fs)
	_ = fs.Parse(args)

	store, _, closeStore := resolveStore("user list", kf, *dbPath)
	defer closeStore()

	users, err := store.ListUsers()
	if err != nil {
		fmt.Fprintf(os.Stderr, "membershipd user list: %v\n", err)
		os.Exit(1)
	}
	if len(users) == 0 {
		fmt.Println("(no users)")
		return
	}
	w := tabwriter.NewWriter(os.Stdout, 0, 2, 2, ' ', 0)
	fmt.Fprintln(w, "HANDLE\tROLE\tSTATUS\tSIGN_PUB\tCREATED")
	for _, u := range users {
		fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", u.Handle, u.Role, u.Status, u.SignPub, u.CreatedAt)
	}
	_ = w.Flush()
}

func userRevoke(args []string) {
	fs := flag.NewFlagSet("user revoke", flag.ExitOnError)
	dbPath := fs.String("db", defaultDBPath, "SQLite database path")
	kf := registerKVFlags(fs)

	// Go's flag package stops at the first non-flag argument, so `revoke <key>
	// --db path` would otherwise leave --db unparsed. Pull a leading positional
	// (the sign-pub) off the front before parsing so both `revoke <key> --db p`
	// and `revoke --db p <key>` work for the operator.
	var signPub string
	if len(args) > 0 && !strings.HasPrefix(args[0], "-") {
		signPub, args = args[0], args[1:]
	}
	_ = fs.Parse(args)
	if signPub == "" {
		if rest := fs.Args(); len(rest) == 1 {
			signPub = rest[0]
		}
	}
	if signPub == "" {
		fmt.Fprintln(os.Stderr, "membershipd user revoke: exactly one <sign-pub> argument required")
		os.Exit(2)
	}
	if err := validateSignPubHex(signPub); err != nil {
		fmt.Fprintf(os.Stderr, "membershipd user revoke: %v\n", err)
		os.Exit(2)
	}

	store, _, closeStore := resolveStore("user revoke", kf, *dbPath)
	defer closeStore()

	if err := store.RevokeUser(signPub); err != nil {
		fmt.Fprintf(os.Stderr, "membershipd user revoke: %v\n", err)
		os.Exit(1)
	}
	fmt.Printf("revoked user %s\n", signPub)
}
