// Package cli: acl.go implements the `orca acl` subcommand family
// (P02, v0.11). Subcommands:
//
//	orca acl grant <identity> --namespace <ns> --permissions <perms>
//	orca acl revoke <identity> --namespace <ns>
//	orca acl list
//	orca acl check <identity> --namespace <ns> --permission <perm>
//
// ACL state is stored at paths.ClusterDir()/acl.json (a simple JSON
// file — no DB needed for v0.11). <identity> is either a SPIFFE URI
// (spiffe://orca.local/ns/.../sa/.../...) or a bare token ID.
package cli

import (
	"encoding/json"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"
	"strings"

	"github.com/spf13/cobra"

	"git.cloudinit.dev/coreci/orca/internal/acl"
	"git.cloudinit.dev/coreci/orca/internal/paths"
)

var (
	aclGrantNamespace   string
	aclGrantPermissions string
	aclRevokeNamespace  string
	aclCheckNamespace   string
	aclCheckPermission  string
)

var aclCmd = &cobra.Command{
	Use:   "acl",
	Short: "Manage access-control entries (SPIFFE + token identities)",
	Long: `Manage the cluster ACL (P02, v0.11). Identities are either
SPIFFE workload URIs (spiffe://orca.local/ns/<ns>/sa/<sa>/<alloc>) or
operator token IDs. Permissions are deny-by-default: an identity with
no matching entry on a namespace has no access.

State is stored at ` + "`" + `ClusterDir()/acl.json` + "`" + `.`,
}

// parseIdentity classifies <identity> as a SPIFFE or token identity.
// A SPIFFE identity is detected by the spiffe:// scheme; its namespace
// is extracted from the URI path. Anything else is treated as a token
// ID whose namespace must be supplied via the --namespace flag.
func parseIdentity(raw string) (acl.Identity, error) {
	if strings.HasPrefix(raw, "spiffe://") {
		ns, err := acl.SpiffeNamespace(raw)
		if err != nil {
			return acl.Identity{}, fmt.Errorf("parse spiffe identity: %w", err)
		}
		return acl.Identity{Kind: acl.KindSpiffe, ID: raw, Namespace: ns}, nil
	}
	if raw == "" {
		return acl.Identity{}, fmt.Errorf("identity is empty")
	}
	return acl.Identity{Kind: acl.KindOidc, ID: raw}, nil
}

// parsePermissions parses a comma-separated list of "read","write",
// "admin" into a Permission bitmask. Empty string defaults to read.
func parsePermissions(s string) (acl.Permission, error) {
	s = strings.TrimSpace(s)
	if s == "" {
		return acl.PermRead, nil
	}
	var perms acl.Permission
	for _, part := range strings.Split(s, ",") {
		part = strings.TrimSpace(strings.ToLower(part))
		switch part {
		case "read":
			perms |= acl.PermRead
		case "write":
			perms |= acl.PermWrite
		case "admin":
			perms |= acl.PermAdmin
		default:
			return 0, fmt.Errorf("unknown permission %q (want read, write, or admin)", part)
		}
	}
	if perms == 0 {
		return 0, fmt.Errorf("no permissions in %q", s)
	}
	return perms, nil
}

// permName renders a Permission bitmask as a comma-separated string.
func permName(p acl.Permission) string {
	var parts []string
	if p&acl.PermRead != 0 {
		parts = append(parts, "read")
	}
	if p&acl.PermWrite != 0 {
		parts = append(parts, "write")
	}
	if p&acl.PermAdmin != 0 {
		parts = append(parts, "admin")
	}
	if len(parts) == 0 {
		return "none"
	}
	return strings.Join(parts, ",")
}

// aclState is the on-disk JSON shape for acl.json.
type aclState struct {
	Entries []acl.ACLEntry `json:"entries"`
}

// loadACL reads paths.ACLPath() and returns an *acl.ACL. A missing
// file is treated as an empty ACL (not an error).
func loadACL() (*acl.ACL, error) {
	a := acl.NewACL()
	path := paths.ACLPath()
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return a, nil
		}
		return nil, fmt.Errorf("read acl state: %w", err)
	}
	if len(data) == 0 {
		return a, nil
	}
	var st aclState
	if err := json.Unmarshal(data, &st); err != nil {
		return nil, fmt.Errorf("parse acl state: %w", err)
	}
	for _, e := range st.Entries {
		a.Grant(e.Identity, e.Namespace, e.Permissions)
	}
	return a, nil
}

// saveACL writes the ACL to paths.ACLPath() atomically (write to temp,
// rename). The cluster dir is created if missing.
func saveACL(a *acl.ACL) error {
	path := paths.ACLPath()
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return fmt.Errorf("create cluster dir: %w", err)
	}
	st := aclState{Entries: a.List()}
	data, err := json.MarshalIndent(st, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal acl state: %w", err)
	}
	if err := writeAtomicFile(path, data, 0o644); err != nil {
		return fmt.Errorf("write acl state: %w", err)
	}
	return nil
}

// writeAtomicFile writes data to a temp file in dir(path) and renames
// it into place, matching the security.WriteAtomic pattern (P02 keeps
// a local copy to avoid importing internal/security into the CLI).
func writeAtomicFile(path string, data []byte, mode os.FileMode) error {
	dir := filepath.Dir(path)
	tmp, err := os.CreateTemp(dir, ".acl-tmp-*")
	if err != nil {
		return fmt.Errorf("create temp: %w", err)
	}
	tmpName := tmp.Name()
	defer func() { _ = os.Remove(tmpName) }()
	if _, err := tmp.Write(data); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("write temp: %w", err)
	}
	if err := tmp.Chmod(mode); err != nil {
		_ = tmp.Close()
		return fmt.Errorf("chmod temp: %w", err)
	}
	if err := tmp.Close(); err != nil {
		return fmt.Errorf("close temp: %w", err)
	}
	if err := os.Rename(tmpName, path); err != nil {
		return fmt.Errorf("rename temp: %w", err)
	}
	return nil
}

var aclGrantCmd = &cobra.Command{
	Use:   "grant <identity>",
	Short: "Grant permissions to an identity on a namespace",
	Long: `Grant permissions to an identity on a namespace. The identity
is either a SPIFFE URI (its namespace is extracted from the path and
must match --namespace) or a bare token ID (whose namespace is
--namespace). --permissions is a comma-separated list of read,write,
admin (default: read).`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		identity, err := parseIdentity(args[0])
		if err != nil {
			return err
		}
		ns := aclGrantNamespace
		if ns == "" {
			ns = identity.Namespace
		}
		if ns == "" {
			return fmt.Errorf("--namespace is required for token identities (or set it to match the spiffe path)")
		}
		if identity.Kind == acl.KindSpiffe && identity.Namespace != "" && identity.Namespace != ns {
			return fmt.Errorf("spiffe namespace %q does not match --namespace %q", identity.Namespace, ns)
		}
		perms, err := parsePermissions(aclGrantPermissions)
		if err != nil {
			return err
		}
		a, err := loadACL()
		if err != nil {
			return err
		}
		identity.Namespace = ns
		a.Grant(identity, ns, perms)
		if err := saveACL(a); err != nil {
			return err
		}
		slog.Info("acl grant", "identity", identity.ID, "namespace", ns, "permissions", permName(perms))
		if jsonOutput {
			return printJSON(map[string]any{
				"identity":    identity,
				"namespace":   ns,
				"permissions": permName(perms),
				"granted":     true,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Granted %s on %s to %s\n", permName(perms), ns, identity.ID)
		return nil
	},
}

var aclRevokeCmd = &cobra.Command{
	Use:   "revoke <identity>",
	Short: "Revoke an identity's access on a namespace",
	Long: `Revoke an identity's entry on a namespace. For a SPIFFE
identity the namespace defaults to the one in the URI path; for a
token identity --namespace is required.`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		identity, err := parseIdentity(args[0])
		if err != nil {
			return err
		}
		ns := aclRevokeNamespace
		if ns == "" {
			ns = identity.Namespace
		}
		if ns == "" {
			return fmt.Errorf("--namespace is required for token identities")
		}
		a, err := loadACL()
		if err != nil {
			return err
		}
		identity.Namespace = ns
		a.Revoke(identity, ns)
		if err := saveACL(a); err != nil {
			return err
		}
		slog.Info("acl revoke", "identity", identity.ID, "namespace", ns)
		if jsonOutput {
			return printJSON(map[string]any{
				"identity":  identity,
				"namespace": ns,
				"revoked":   true,
			})
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✓ Revoked %s on %s\n", identity.ID, ns)
		return nil
	},
}

var aclListCmd = &cobra.Command{
	Use:   "list",
	Short: "List all ACL entries",
	Args:  cobra.NoArgs,
	RunE: func(cmd *cobra.Command, args []string) error {
		a, err := loadACL()
		if err != nil {
			return err
		}
		entries := a.List()
		if jsonOutput {
			return printJSON(entries)
		}
		out := cmd.OutOrStdout()
		if len(entries) == 0 {
			fmt.Fprintln(out, "No ACL entries. Use `orca acl grant` to add one.")
			return nil
		}
		fmt.Fprintf(out, "%-12s  %-50s  %-16s  %s\n", "KIND", "IDENTITY", "NAMESPACE", "PERMISSIONS")
		for _, e := range entries {
			fmt.Fprintf(out, "%-12s  %-50s  %-16s  %s\n", e.Identity.Kind, e.Identity.ID, e.Namespace, permName(e.Permissions))
		}
		return nil
	},
}

var aclCheckCmd = &cobra.Command{
	Use:   "check <identity>",
	Short: "Check whether an identity has a permission on a namespace",
	Long: `Check whether an identity has the given permission on the
namespace. Exits 0 if allowed, 1 if denied. --permission is one of
read, write, admin (default: read).`,
	Args: cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		identity, err := parseIdentity(args[0])
		if err != nil {
			return err
		}
		ns := aclCheckNamespace
		if ns == "" {
			ns = identity.Namespace
		}
		if ns == "" {
			return fmt.Errorf("--namespace is required for token identities")
		}
		permStr := strings.TrimSpace(aclCheckPermission)
		if permStr == "" {
			permStr = "read"
		}
		perm, err := parsePermissions(permStr)
		if err != nil {
			return err
		}
		a, err := loadACL()
		if err != nil {
			return err
		}
		identity.Namespace = ns
		allowed := a.Check(identity, ns, perm)
		if jsonOutput {
			return printJSON(map[string]any{
				"identity":   identity,
				"namespace":  ns,
				"permission": permStr,
				"allowed":    allowed,
			})
		}
		if allowed {
			fmt.Fprintf(cmd.OutOrStdout(), "✓ %s has %s on %s\n", identity.ID, permStr, ns)
			return nil
		}
		fmt.Fprintf(cmd.OutOrStdout(), "✗ %s does NOT have %s on %s\n", identity.ID, permStr, ns)
		return fmt.Errorf("denied")
	},
}

func init() {
	aclGrantCmd.Flags().StringVar(&aclGrantNamespace, "namespace", "", "namespace scope (required for tokens; defaults to spiffe path ns)")
	aclGrantCmd.Flags().StringVar(&aclGrantPermissions, "permissions", "read", "comma-separated permissions: read,write,admin")
	aclRevokeCmd.Flags().StringVar(&aclRevokeNamespace, "namespace", "", "namespace scope (required for tokens; defaults to spiffe path ns)")
	aclCheckCmd.Flags().StringVar(&aclCheckNamespace, "namespace", "", "namespace scope (required for tokens; defaults to spiffe path ns)")
	aclCheckCmd.Flags().StringVar(&aclCheckPermission, "permission", "read", "permission to check: read, write, or admin")

	aclCmd.AddCommand(aclGrantCmd)
	aclCmd.AddCommand(aclRevokeCmd)
	aclCmd.AddCommand(aclListCmd)
	aclCmd.AddCommand(aclCheckCmd)
	rootCmd.AddCommand(aclCmd)
}
