// Command mciasctl is the MCIAS admin CLI.
//
// It connects to a running mciassrv instance and provides subcommands for
// managing accounts, roles, tokens, Postgres credentials, policy rules, and
// account tags.
//
// Usage:
//
//	mciasctl [global flags] <command> [args]
//
// Global flags:
//
//	--server  URL of the mciassrv instance (default: https://mcias.metacircular.net:8443)
//	--token   Bearer token for authentication (or set MCIAS_TOKEN env var)
//	--cacert  Path to CA certificate for TLS verification (optional)
//
// Commands:
//
//	auth login           --username NAME [--totp CODE]
//	auth change-password (passwords always prompted interactively)
//
//	account list
//	account create       --username NAME [--type human|system]
//	account get          --id UUID
//	account update       --id UUID [--status active|inactive]
//	account delete       --id UUID
//	account set-password --id UUID
//
//	role list   --id UUID
//	role set    --id UUID --roles role1,role2,...
//	role grant  --id UUID --role ROLE
//	role revoke --id UUID --role ROLE
//
//	token issue  --id UUID
//	token revoke --jti JTI
//
//	pgcreds list
//	pgcreds set --id UUID --host HOST [--port PORT] --db DB --user USER [--password PASS]
//	pgcreds get --id UUID
//
//	policy list
//	policy create  --description STR --json FILE [--priority N] [--not-before RFC3339] [--expires-at RFC3339]
//	policy get     --id ID
//	policy update  --id ID [--priority N] [--enabled true|false] [--not-before RFC3339] [--expires-at RFC3339] [--clear-not-before] [--clear-expires-at]
//	policy delete  --id ID
//
//	tag list    --id UUID
//	tag set     --id UUID --tags tag1,tag2,...
package main

import (
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/spf13/cobra"

	"git.wntrmute.dev/mc/mcdsl/terminal"
)

// Global flags bound by the root command's PersistentFlags.
var (
	serverURL  string
	tokenFlag  string
	caCertPath string
)

func main() {
	root := &cobra.Command{
		Use:   "mciasctl",
		Short: "MCIAS admin CLI",
		Long:  "mciasctl connects to a running mciassrv instance and provides subcommands\nfor managing accounts, roles, tokens, Postgres credentials, policy rules,\nand account tags.",
	}

	root.PersistentFlags().StringVar(&serverURL, "server", "https://mcias.metacircular.net:8443", "mciassrv base URL")
	root.PersistentFlags().StringVar(&tokenFlag, "token", "", "bearer token (or set MCIAS_TOKEN)")
	root.PersistentFlags().StringVar(&caCertPath, "cacert", "", "path to CA certificate for TLS")

	root.AddCommand(authCmd())
	root.AddCommand(accountCmd())
	root.AddCommand(roleCmd())
	root.AddCommand(tokenCmd())
	root.AddCommand(pgcredsCmd())
	root.AddCommand(policyCmd())
	root.AddCommand(tagCmd())
	root.AddCommand(ssoCmd())

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

// newController builds a controller from the global flags. Called at the start
// of every leaf command's Run function.
func newController() *controller {
	// Resolve token from flag or environment.
	bearerToken := tokenFlag
	if bearerToken == "" {
		bearerToken = os.Getenv("MCIAS_TOKEN")
	}

	client, err := newHTTPClient(caCertPath)
	if err != nil {
		fatalf("build HTTP client: %v", err)
	}

	return &controller{
		serverURL: strings.TrimRight(serverURL, "/"),
		token:     bearerToken,
		client:    client,
	}
}

// controller holds shared state for all subcommands.
type controller struct {
	client    *http.Client
	serverURL string
	token     string
}

// ---- auth subcommands ----

func authCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "auth",
		Short: "Authentication commands",
	}
	cmd.AddCommand(authLoginCmd())
	cmd.AddCommand(authChangePasswordCmd())
	return cmd
}

// authLoginCmd authenticates with the server using username and password, then
// prints the resulting bearer token to stdout. The password is always prompted
// interactively; it is never accepted as a command-line flag to prevent it from
// appearing in shell history, ps output, and process argument lists.
//
// Security: terminal echo is disabled during password entry
// (mcdsl/terminal.ReadPassword).
func authLoginCmd() *cobra.Command {
	var username string
	var totpCode string

	cmd := &cobra.Command{
		Use:   "login",
		Short: "Obtain a bearer token",
		Long: `Obtain a bearer token. Password is always prompted interactively
(never accepted as a flag) to avoid shell-history exposure.
Token is written to stdout; expiry to stderr.
Example: export MCIAS_TOKEN=$(mciasctl auth login --username alice)`,
		Run: func(cmd *cobra.Command, args []string) {
			if username == "" {
				fatalf("auth login: --username is required")
			}

			c := newController()

			// Security: always prompt interactively; never accept password as a flag.
			// This prevents the credential from appearing in shell history, ps output,
			// and /proc/PID/cmdline.
			passwd, err := terminal.ReadPassword("Password: ")
			if err != nil {
				fatalf("read password: %v", err)
			}

			body := map[string]string{
				"username": username,
				"password": passwd,
			}
			if totpCode != "" {
				body["totp_code"] = totpCode
			}

			var result struct {
				Token     string `json:"token"`
				ExpiresAt string `json:"expires_at"`
			}
			c.doRequest("POST", "/v1/auth/login", body, &result)

			// Print token to stdout so it can be captured by scripts, e.g.:
			//   export MCIAS_TOKEN=$(mciasctl auth login --username alice)
			fmt.Println(result.Token)
			if result.ExpiresAt != "" {
				fmt.Fprintf(os.Stderr, "expires: %s\n", result.ExpiresAt)
			}
		},
	}
	cmd.Flags().StringVar(&username, "username", "", "username (required)")
	cmd.Flags().StringVar(&totpCode, "totp", "", "TOTP code (required if TOTP is enrolled)")
	return cmd
}

// authChangePasswordCmd allows an authenticated user to change their own password.
// A valid bearer token must be set (via --token flag or MCIAS_TOKEN env var).
// Both passwords are always prompted interactively; they are never accepted as
// command-line flags to prevent them from appearing in shell history, ps
// output, and process argument lists.
//
// Security: terminal echo is disabled during entry
// (mcdsl/terminal.ReadPassword).  The server requires the current password
// to prevent token-theft attacks.  On success all other active sessions are
// revoked server-side.
func authChangePasswordCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "change-password",
		Short: "Change the current user's own password",
		Long: `Change the current user's own password. Requires a valid bearer
token. Current and new passwords are always prompted interactively.
Revokes all other active sessions on success.`,
		Run: func(cmd *cobra.Command, args []string) {
			c := newController()

			// Security: always prompt interactively; never accept passwords as flags.
			currentPasswd, err := terminal.ReadPassword("Current password: ")
			if err != nil {
				fatalf("read current password: %v", err)
			}

			newPasswd, err := terminal.ReadPassword("New password: ")
			if err != nil {
				fatalf("read new password: %v", err)
			}

			body := map[string]string{
				"current_password": currentPasswd,
				"new_password":     newPasswd,
			}
			c.doRequest("PUT", "/v1/auth/password", body, nil)
			fmt.Println("password changed; other active sessions revoked")
		},
	}
}

// ---- account subcommands ----

func accountCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "account",
		Short: "Account management commands",
	}
	cmd.AddCommand(accountListCmd())
	cmd.AddCommand(accountCreateCmd())
	cmd.AddCommand(accountGetCmd())
	cmd.AddCommand(accountUpdateCmd())
	cmd.AddCommand(accountDeleteCmd())
	cmd.AddCommand(accountSetPasswordCmd())
	return cmd
}

func accountListCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all accounts",
		Run: func(cmd *cobra.Command, args []string) {
			c := newController()
			var result []json.RawMessage
			c.doRequest("GET", "/v1/accounts", nil, &result)
			printJSON(result)
		},
	}
}

func accountCreateCmd() *cobra.Command {
	var username string
	var accountType string

	cmd := &cobra.Command{
		Use:   "create",
		Short: "Create a new account",
		Run: func(cmd *cobra.Command, args []string) {
			if username == "" {
				fatalf("account create: --username is required")
			}

			c := newController()

			// Security: always prompt interactively for human-account passwords; never
			// accept them as a flag.  Terminal echo is disabled via
			// mcdsl/terminal.ReadPassword.  System accounts have no password.
			var passwd string
			if accountType == "human" {
				var err error
				passwd, err = terminal.ReadPassword("Password: ")
				if err != nil {
					fatalf("read password: %v", err)
				}
			}

			body := map[string]string{
				"username":     username,
				"account_type": accountType,
			}
			if passwd != "" {
				body["password"] = passwd
			}

			var result json.RawMessage
			c.doRequest("POST", "/v1/accounts", body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&username, "username", "", "username (required)")
	cmd.Flags().StringVar(&accountType, "type", "human", "account type: human or system")
	return cmd
}

func accountGetCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get account details",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("account get: --id is required")
			}

			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/accounts/"+id, nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

func accountUpdateCmd() *cobra.Command {
	var id string
	var status string

	cmd := &cobra.Command{
		Use:   "update",
		Short: "Update an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("account update: --id is required")
			}
			if status == "" {
				fatalf("account update: --status is required")
			}

			c := newController()
			body := map[string]string{"status": status}
			c.doRequest("PATCH", "/v1/accounts/"+id, body, nil)
			fmt.Println("account updated")
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&status, "status", "", "new status: active or inactive")
	return cmd
}

func accountDeleteCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "delete",
		Short: "Delete an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("account delete: --id is required")
			}

			c := newController()
			c.doRequest("DELETE", "/v1/accounts/"+id, nil, nil)
			fmt.Println("account deleted")
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

// accountSetPasswordCmd resets a human account's password (admin operation).
// No current password is required.  All active sessions for the target account
// are revoked by the server on success.
//
// Security: the new password is always prompted interactively; it is never
// accepted as a command-line flag to prevent it from appearing in shell
// history, ps output, and process argument lists.  Terminal echo is disabled
// (mcdsl/terminal.ReadPassword).
func accountSetPasswordCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "set-password",
		Short: "Reset a human account's password (admin)",
		Long: `Admin: reset a human account's password without requiring the
current password. New password is always prompted interactively.
Revokes all active sessions for the account.`,
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("account set-password: --id is required")
			}

			c := newController()

			// Security: always prompt interactively; never accept password as a flag.
			passwd, err := terminal.ReadPassword("New password: ")
			if err != nil {
				fatalf("read password: %v", err)
			}

			body := map[string]string{"new_password": passwd}
			c.doRequest("PUT", "/v1/accounts/"+id+"/password", body, nil)
			fmt.Println("password updated; all active sessions revoked")
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

// ---- role subcommands ----

func roleCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "role",
		Short: "Role management commands",
	}
	cmd.AddCommand(roleListCmd())
	cmd.AddCommand(roleSetCmd())
	cmd.AddCommand(roleGrantCmd())
	cmd.AddCommand(roleRevokeCmd())
	return cmd
}

func roleListCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "list",
		Short: "List roles for an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("role list: --id is required")
			}

			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/accounts/"+id+"/roles", nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

func roleSetCmd() *cobra.Command {
	var id string
	var rolesFlag string

	cmd := &cobra.Command{
		Use:   "set",
		Short: "Set roles for an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("role set: --id is required")
			}

			c := newController()

			roles := []string{}
			if rolesFlag != "" {
				for _, r := range strings.Split(rolesFlag, ",") {
					r = strings.TrimSpace(r)
					if r != "" {
						roles = append(roles, r)
					}
				}
			}

			body := map[string][]string{"roles": roles}
			c.doRequest("PUT", "/v1/accounts/"+id+"/roles", body, nil)
			fmt.Printf("roles set: %v\n", roles)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&rolesFlag, "roles", "", "comma-separated list of roles")
	return cmd
}

func roleGrantCmd() *cobra.Command {
	var id string
	var role string

	cmd := &cobra.Command{
		Use:   "grant",
		Short: "Grant a role to an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("role grant: --id is required")
			}
			if role == "" {
				fatalf("role grant: --role is required")
			}

			c := newController()
			body := map[string]string{"role": role}
			c.doRequest("POST", "/v1/accounts/"+id+"/roles", body, nil)
			fmt.Printf("role granted: %s\n", role)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&role, "role", "", "role name (required)")
	return cmd
}

func roleRevokeCmd() *cobra.Command {
	var id string
	var role string

	cmd := &cobra.Command{
		Use:   "revoke",
		Short: "Revoke a role from an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("role revoke: --id is required")
			}
			if role == "" {
				fatalf("role revoke: --role is required")
			}

			c := newController()
			c.doRequest("DELETE", "/v1/accounts/"+id+"/roles/"+role, nil, nil)
			fmt.Printf("role revoked: %s\n", role)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&role, "role", "", "role name (required)")
	return cmd
}

// ---- token subcommands ----

func tokenCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "token",
		Short: "Token management commands",
	}
	cmd.AddCommand(tokenIssueCmd())
	cmd.AddCommand(tokenRevokeCmd())
	return cmd
}

func tokenIssueCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "issue",
		Short: "Issue a token for a system account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("token issue: --id is required")
			}

			c := newController()
			body := map[string]string{"account_id": id}
			var result json.RawMessage
			c.doRequest("POST", "/v1/token/issue", body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "system account UUID (required)")
	return cmd
}

func tokenRevokeCmd() *cobra.Command {
	var jti string

	cmd := &cobra.Command{
		Use:   "revoke",
		Short: "Revoke a token by JTI",
		Run: func(cmd *cobra.Command, args []string) {
			if jti == "" {
				fatalf("token revoke: --jti is required")
			}

			c := newController()
			c.doRequest("DELETE", "/v1/token/"+jti, nil, nil)
			fmt.Println("token revoked")
		},
	}
	cmd.Flags().StringVar(&jti, "jti", "", "JTI of the token to revoke (required)")
	return cmd
}

// ---- pgcreds subcommands ----

func pgcredsCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "pgcreds",
		Short: "Postgres credential management commands",
	}
	cmd.AddCommand(pgcredsListCmd())
	cmd.AddCommand(pgcredsGetCmd())
	cmd.AddCommand(pgcredsSetCmd())
	return cmd
}

func pgcredsListCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all Postgres credentials",
		Run: func(cmd *cobra.Command, args []string) {
			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/pgcreds", nil, &result)
			printJSON(result)
		},
	}
}

func pgcredsGetCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get Postgres credentials for an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("pgcreds get: --id is required")
			}

			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/accounts/"+id+"/pgcreds", nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

func pgcredsSetCmd() *cobra.Command {
	var id string
	var host string
	var port int
	var dbName string
	var username string
	var password string

	cmd := &cobra.Command{
		Use:   "set",
		Short: "Set Postgres credentials for an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" || host == "" || dbName == "" || username == "" {
				fatalf("pgcreds set: --id, --host, --db, and --user are required")
			}

			c := newController()

			// Prompt for the Postgres password interactively if not supplied so it
			// stays out of shell history.
			// Security: terminal echo is disabled during entry via
			// mcdsl/terminal.ReadPassword.
			passwd := password
			if passwd == "" {
				var err error
				passwd, err = terminal.ReadPassword("Postgres password: ")
				if err != nil {
					fatalf("read password: %v", err)
				}
			}

			body := map[string]interface{}{
				"host":     host,
				"port":     port,
				"database": dbName,
				"username": username,
				"password": passwd,
			}
			c.doRequest("PUT", "/v1/accounts/"+id+"/pgcreds", body, nil)
			fmt.Println("credentials stored")
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&host, "host", "", "Postgres host (required)")
	cmd.Flags().IntVar(&port, "port", 5432, "Postgres port")
	cmd.Flags().StringVar(&dbName, "db", "", "Postgres database name (required)")
	cmd.Flags().StringVar(&username, "user", "", "Postgres username (required)")
	cmd.Flags().StringVar(&password, "password", "", "Postgres password (prompted if omitted)")
	return cmd
}

// ---- policy subcommands ----

func policyCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "policy",
		Short: "Policy rule management commands",
	}
	cmd.AddCommand(policyListCmd())
	cmd.AddCommand(policyCreateCmd())
	cmd.AddCommand(policyGetCmd())
	cmd.AddCommand(policyUpdateCmd())
	cmd.AddCommand(policyDeleteCmd())
	return cmd
}

func policyListCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all policy rules",
		Run: func(cmd *cobra.Command, args []string) {
			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/policy/rules", nil, &result)
			printJSON(result)
		},
	}
}

func policyCreateCmd() *cobra.Command {
	var description string
	var jsonFile string
	var priority int
	var notBefore string
	var expiresAt string

	cmd := &cobra.Command{
		Use:   "create",
		Short: "Create a new policy rule",
		Run: func(cmd *cobra.Command, args []string) {
			if description == "" {
				fatalf("policy create: --description is required")
			}
			if jsonFile == "" {
				fatalf("policy create: --json is required (path to rule body JSON file)")
			}

			c := newController()

			// G304: path comes from a CLI flag supplied by the operator.
			ruleBytes, err := os.ReadFile(jsonFile) //nolint:gosec
			if err != nil {
				fatalf("policy create: read %s: %v", jsonFile, err)
			}

			// Validate that the file contains valid JSON before sending.
			var ruleBody json.RawMessage
			if err := json.Unmarshal(ruleBytes, &ruleBody); err != nil {
				fatalf("policy create: invalid JSON in %s: %v", jsonFile, err)
			}

			body := map[string]interface{}{
				"description": description,
				"priority":    priority,
				"rule":        ruleBody,
			}
			if notBefore != "" {
				if _, err := time.Parse(time.RFC3339, notBefore); err != nil {
					fatalf("policy create: --not-before must be RFC3339: %v", err)
				}
				body["not_before"] = notBefore
			}
			if expiresAt != "" {
				if _, err := time.Parse(time.RFC3339, expiresAt); err != nil {
					fatalf("policy create: --expires-at must be RFC3339: %v", err)
				}
				body["expires_at"] = expiresAt
			}

			var result json.RawMessage
			c.doRequest("POST", "/v1/policy/rules", body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&description, "description", "", "rule description (required)")
	cmd.Flags().StringVar(&jsonFile, "json", "", "path to JSON file containing the rule body (required)")
	cmd.Flags().IntVar(&priority, "priority", 100, "rule priority (lower = evaluated first)")
	cmd.Flags().StringVar(&notBefore, "not-before", "", "earliest activation time (RFC3339, optional)")
	cmd.Flags().StringVar(&expiresAt, "expires-at", "", "expiry time (RFC3339, optional)")
	return cmd
}

func policyGetCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get a policy rule",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("policy get: --id is required")
			}

			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/policy/rules/"+id, nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "rule ID (required)")
	return cmd
}

func policyUpdateCmd() *cobra.Command {
	var id string
	var priority int
	var enabled string
	var notBefore string
	var expiresAt string
	var clearNotBefore bool
	var clearExpiresAt bool

	cmd := &cobra.Command{
		Use:   "update",
		Short: "Update a policy rule",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("policy update: --id is required")
			}

			c := newController()

			body := map[string]interface{}{}
			if priority >= 0 {
				body["priority"] = priority
			}
			if enabled != "" {
				switch enabled {
				case "true":
					b := true
					body["enabled"] = b
				case "false":
					b := false
					body["enabled"] = b
				default:
					fatalf("policy update: --enabled must be true or false")
				}
			}
			if clearNotBefore {
				body["clear_not_before"] = true
			} else if notBefore != "" {
				if _, err := time.Parse(time.RFC3339, notBefore); err != nil {
					fatalf("policy update: --not-before must be RFC3339: %v", err)
				}
				body["not_before"] = notBefore
			}
			if clearExpiresAt {
				body["clear_expires_at"] = true
			} else if expiresAt != "" {
				if _, err := time.Parse(time.RFC3339, expiresAt); err != nil {
					fatalf("policy update: --expires-at must be RFC3339: %v", err)
				}
				body["expires_at"] = expiresAt
			}
			if len(body) == 0 {
				fatalf("policy update: at least one flag is required")
			}

			var result json.RawMessage
			c.doRequest("PATCH", "/v1/policy/rules/"+id, body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "rule ID (required)")
	cmd.Flags().IntVar(&priority, "priority", -1, "new priority (-1 = no change)")
	cmd.Flags().StringVar(&enabled, "enabled", "", "true or false")
	cmd.Flags().StringVar(&notBefore, "not-before", "", "earliest activation time (RFC3339)")
	cmd.Flags().StringVar(&expiresAt, "expires-at", "", "expiry time (RFC3339)")
	cmd.Flags().BoolVar(&clearNotBefore, "clear-not-before", false, "remove not_before constraint")
	cmd.Flags().BoolVar(&clearExpiresAt, "clear-expires-at", false, "remove expires_at constraint")
	return cmd
}

func policyDeleteCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "delete",
		Short: "Delete a policy rule",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("policy delete: --id is required")
			}

			c := newController()
			c.doRequest("DELETE", "/v1/policy/rules/"+id, nil, nil)
			fmt.Println("policy rule deleted")
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "rule ID (required)")
	return cmd
}

// ---- tag subcommands ----

func tagCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "tag",
		Short: "Account tag management commands",
	}
	cmd.AddCommand(tagListCmd())
	cmd.AddCommand(tagSetCmd())
	return cmd
}

func tagListCmd() *cobra.Command {
	var id string

	cmd := &cobra.Command{
		Use:   "list",
		Short: "List tags for an account",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("tag list: --id is required")
			}

			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/accounts/"+id+"/tags", nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	return cmd
}

func tagSetCmd() *cobra.Command {
	var id string
	var tagsFlag string

	cmd := &cobra.Command{
		Use:   "set",
		Short: "Set tags for an account",
		Long:  "Set tags for an account. Pass empty --tags \"\" to clear all tags.",
		Run: func(cmd *cobra.Command, args []string) {
			if id == "" {
				fatalf("tag set: --id is required")
			}

			c := newController()

			tags := []string{}
			if tagsFlag != "" {
				for _, t := range strings.Split(tagsFlag, ",") {
					t = strings.TrimSpace(t)
					if t != "" {
						tags = append(tags, t)
					}
				}
			}

			body := map[string][]string{"tags": tags}
			c.doRequest("PUT", "/v1/accounts/"+id+"/tags", body, nil)
			fmt.Printf("tags set: %v\n", tags)
		},
	}
	cmd.Flags().StringVar(&id, "id", "", "account UUID (required)")
	cmd.Flags().StringVar(&tagsFlag, "tags", "", "comma-separated list of tags (empty string clears all tags)")
	return cmd
}

// ---- SSO client commands ----

func ssoCmd() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "sso",
		Short: "SSO client management commands",
	}
	cmd.AddCommand(ssoListCmd())
	cmd.AddCommand(ssoCreateCmd())
	cmd.AddCommand(ssoGetCmd())
	cmd.AddCommand(ssoUpdateCmd())
	cmd.AddCommand(ssoDeleteCmd())
	return cmd
}

func ssoListCmd() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all SSO clients",
		Run: func(cmd *cobra.Command, args []string) {
			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/sso/clients", nil, &result)
			printJSON(result)
		},
	}
}

func ssoCreateCmd() *cobra.Command {
	var clientID, redirectURI, tagsFlag string

	cmd := &cobra.Command{
		Use:   "create",
		Short: "Register a new SSO client",
		Run: func(cmd *cobra.Command, args []string) {
			if clientID == "" {
				fatalf("sso create: --client-id is required")
			}
			if redirectURI == "" {
				fatalf("sso create: --redirect-uri is required")
			}

			c := newController()

			body := map[string]interface{}{
				"client_id":    clientID,
				"redirect_uri": redirectURI,
			}
			if tagsFlag != "" {
				tags := []string{}
				for _, t := range strings.Split(tagsFlag, ",") {
					t = strings.TrimSpace(t)
					if t != "" {
						tags = append(tags, t)
					}
				}
				body["tags"] = tags
			}

			var result json.RawMessage
			c.doRequest("POST", "/v1/sso/clients", body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&clientID, "client-id", "", "SSO client identifier / service name (required)")
	cmd.Flags().StringVar(&redirectURI, "redirect-uri", "", "callback URL (https:// required)")
	cmd.Flags().StringVar(&tagsFlag, "tags", "", "comma-separated list of tags")
	return cmd
}

func ssoGetCmd() *cobra.Command {
	var clientID string

	cmd := &cobra.Command{
		Use:   "get",
		Short: "Get an SSO client by client-id",
		Run: func(cmd *cobra.Command, args []string) {
			if clientID == "" {
				fatalf("sso get: --client-id is required")
			}
			c := newController()
			var result json.RawMessage
			c.doRequest("GET", "/v1/sso/clients/"+clientID, nil, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&clientID, "client-id", "", "SSO client identifier (required)")
	return cmd
}

func ssoUpdateCmd() *cobra.Command {
	var clientID, redirectURI, tagsFlag, enabledFlag string

	cmd := &cobra.Command{
		Use:   "update",
		Short: "Update an SSO client",
		Run: func(cmd *cobra.Command, args []string) {
			if clientID == "" {
				fatalf("sso update: --client-id is required")
			}

			c := newController()
			body := map[string]interface{}{}

			if cmd.Flags().Changed("redirect-uri") {
				body["redirect_uri"] = redirectURI
			}
			if cmd.Flags().Changed("tags") {
				tags := []string{}
				if tagsFlag != "" {
					for _, t := range strings.Split(tagsFlag, ",") {
						t = strings.TrimSpace(t)
						if t != "" {
							tags = append(tags, t)
						}
					}
				}
				body["tags"] = tags
			}
			if cmd.Flags().Changed("enabled") {
				body["enabled"] = enabledFlag == "true"
			}

			var result json.RawMessage
			c.doRequest("PATCH", "/v1/sso/clients/"+clientID, body, &result)
			printJSON(result)
		},
	}
	cmd.Flags().StringVar(&clientID, "client-id", "", "SSO client identifier (required)")
	cmd.Flags().StringVar(&redirectURI, "redirect-uri", "", "new callback URL")
	cmd.Flags().StringVar(&tagsFlag, "tags", "", "comma-separated list of tags (empty clears)")
	cmd.Flags().StringVar(&enabledFlag, "enabled", "", "true or false")
	return cmd
}

func ssoDeleteCmd() *cobra.Command {
	var clientID string

	cmd := &cobra.Command{
		Use:   "delete",
		Short: "Delete an SSO client",
		Run: func(cmd *cobra.Command, args []string) {
			if clientID == "" {
				fatalf("sso delete: --client-id is required")
			}
			c := newController()
			c.doRequest("DELETE", "/v1/sso/clients/"+clientID, nil, nil)
			fmt.Printf("SSO client %q deleted\n", clientID)
		},
	}
	cmd.Flags().StringVar(&clientID, "client-id", "", "SSO client identifier (required)")
	return cmd
}

// ---- HTTP helpers ----

// doRequest performs an authenticated JSON HTTP request. If result is non-nil,
// the response body is decoded into it. Exits on error.
func (c *controller) doRequest(method, path string, body, result interface{}) {
	url := c.serverURL + path

	var bodyReader *strings.Reader
	if body != nil {
		b, err := json.Marshal(body)
		if err != nil {
			fatalf("marshal request body: %v", err)
		}
		bodyReader = strings.NewReader(string(b))
	} else {
		bodyReader = strings.NewReader("")
	}

	req, err := http.NewRequest(method, url, bodyReader)
	if err != nil {
		fatalf("create request: %v", err)
	}
	req.Header.Set("Content-Type", "application/json")
	if c.token != "" {
		req.Header.Set("Authorization", "Bearer "+c.token)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		fatalf("HTTP %s %s: %v", method, path, err)
	}
	defer func() { _ = resp.Body.Close() }()

	if resp.StatusCode >= 400 {
		var errBody map[string]string
		_ = json.NewDecoder(resp.Body).Decode(&errBody)
		msg := errBody["error"]
		if msg == "" {
			msg = resp.Status
		}
		fatalf("server returned %d: %s", resp.StatusCode, msg)
	}

	if result != nil && resp.StatusCode != http.StatusNoContent {
		if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
			fatalf("decode response: %v", err)
		}
	}
}

// newHTTPClient builds an http.Client with optional custom CA certificate.
// Security: TLS 1.2+ is required; the system CA pool is used by default.
func newHTTPClient(caCertPath string) (*http.Client, error) {
	tlsCfg := &tls.Config{
		MinVersion: tls.VersionTLS12,
	}

	if caCertPath != "" {
		// G304: path comes from a CLI flag supplied by the operator, not from
		// untrusted input. File inclusion is intentional.
		pemData, err := os.ReadFile(caCertPath) //nolint:gosec
		if err != nil {
			return nil, fmt.Errorf("read CA cert: %w", err)
		}
		pool := x509.NewCertPool()
		if !pool.AppendCertsFromPEM(pemData) {
			return nil, fmt.Errorf("no valid certificates found in %s", caCertPath)
		}
		tlsCfg.RootCAs = pool
	}

	return &http.Client{
		Transport: &http.Transport{
			TLSClientConfig: tlsCfg,
		},
		Timeout: 30 * time.Second,
	}, nil
}

// printJSON pretty-prints a JSON value to stdout.
func printJSON(v interface{}) {
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		fatalf("encode output: %v", err)
	}
}

// fatalf prints an error message and exits with code 1.
func fatalf(format string, args ...interface{}) {
	fmt.Fprintf(os.Stderr, "mciasctl: "+format+"\n", args...)
	os.Exit(1)
}
