// Package cluster holds cluster-wide invariants that are not owned
// by a single subsystem. The first inhabitant is the lead-eligibility
// rule R-003: the cluster lead is always a bare Linux node; Proxmox
// nodes are permanently ineligible because their kernel is shared
// with guest VMs/containers and a lead failure there takes down the
// hypervisor too.
//
// The package is deliberately decoupled from the scheduler: it owns
// its own minimal NodeInfo (Hostname + Kind) so it can be unit-tested
// without pulling in the scheduler's capacity model. The scheduler's
// scheduler.NodeInfo has a `Kind string` field with the same values
// ("linux", "proxmox"); callers convert at the boundary.
package cluster

import (
	"errors"
	"fmt"
	"strings"
)

// NodeKind classifies a node for lead-eligibility purposes (R-003).
// The string values match scheduler.NodeInfo.Kind and model.NodeKind
// so callers can pass either representation through without mapping.
type NodeKind string

const (
	// NodeKindLinux is a bare Linux node — lead-eligible (R-003).
	NodeKindLinux NodeKind = "linux"
	// NodeKindProxmox is a Proxmox VE host — permanently lead-
	// ineligible (R-003): the hypervisor kernel is shared with
	// guests, so a lead process there is a blast-radius hazard.
	NodeKindProxmox NodeKind = "proxmox"
)

// ErrProxmoxNotLead is returned when a Proxmox node is proposed as
// the new cluster lead (R-003).
var ErrProxmoxNotLead = errors.New("Proxmox nodes cannot hold the cluster lead role (R-003)")

// ErrNodeNotRegistered is returned when the proposed lead is not in
// the supplied node list at all.
var ErrNodeNotRegistered = errors.New("cluster: proposed lead is not a registered node")

// NodeInfo is the minimal node projection the lead rules need. It is
// intentionally smaller than scheduler.NodeInfo so this package has
// no upstream dependency on the scheduler.
type NodeInfo struct {
	Hostname string
	Kind     NodeKind
}

// IsLeadEligible reports whether a node of the given kind may hold
// the cluster lead role (R-003). Linux nodes are eligible; Proxmox
// nodes are permanently ineligible; any other kind (including the
// empty string) is treated as ineligible.
func IsLeadEligible(kind NodeKind) bool {
	return kind == NodeKindLinux
}

// ValidateLeadRotation checks that newLead is a registered Linux node
// and refuses Proxmox nodes with ErrProxmoxNotLead (R-003). It returns
// ErrNodeNotRegistered when newLead is not in nodes at all. The check
// is case-sensitive on hostname; node registries in Orca are
// case-normalized at the store layer so this matches reality.
func ValidateLeadRotation(newLead string, nodes []NodeInfo) error {
	for _, n := range nodes {
		if n.Hostname != newLead {
			continue
		}
		if n.Kind == NodeKindProxmox {
			return ErrProxmoxNotLead
		}
		if n.Kind == NodeKindLinux {
			return nil
		}
		// Registered but neither linux nor proxmox (e.g. "localhost"
		// auto-registered node, or a future kind). Treat unknown kinds
		// as ineligible rather than guessing.
		return fmt.Errorf("cluster: node %q has ineligible kind %q: %w", newLead, n.Kind, ErrProxmoxNotLead)
	}
	// Not found in the registry at all.
	return fmt.Errorf("cluster: node %q not found: %w", newLead, ErrNodeNotRegistered)
}

// String renders a NodeKind for logs. It lowercases to match the
// on-disk representation regardless of how the caller constructed it.
func (k NodeKind) String() string { return strings.ToLower(string(k)) }
