// kinds.go — registry of GroupVersionResource (GVR) values that the
// Catalyst-Zero data plane watches on every managed Sovereign cluster.
//
// Per ADR-0001 §5 the kube-apiserver is the system of record for live
// cluster state. Every watched GVR participates in the read path:
//
//	kube-apiserver → SharedInformerFactory → Indexer → SSE → browser
//
// Per docs/INVIOLABLE-PRINCIPLES.md #4 (never hardcode) the default
// registry below is the FALLBACK; the runtime registry is loaded from
// a ConfigMap at factory boot (see factory.go BootKinds). Operators
// extend the watch surface by editing the ConfigMap — no code change.
//
// Two security invariants:
//
//  1. Secret data + stringData fields are NEVER stored in the
//     Indexer's cached object. Even though the watch surfaces Secret
//     metadata so the UI can render existence + age + labels, the
//     snapshot path strips the data block before serialisation. See
//     redactSecretData in snapshot.go for the enforcement point.
//  2. ConfigMap data is similarly stripped on the cache write path so
//     a sensitive value mistakenly stored in a ConfigMap is not
//     leaked through the SSE stream. The UI requests the full body
//     via a separate authenticated GET (with SAR gating) when an
//     operator opens the detail panel.
package k8scache

import (
	"fmt"

	"k8s.io/apimachinery/pkg/runtime/schema"
)

// Kind is a logical name for a watched resource. The wire format
// (events from the SSE endpoint, ConfigMap entries, REST path
// segments) all use this short identifier — never the GVR string.
//
// New kinds are registered by editing DefaultKinds OR by appending to
// the ConfigMap referenced by CATALYST_K8SCACHE_KINDS_CONFIGMAP. The
// short name is canonicalised lower-case (singular) so an operator who
// types "pod", "Pod", or "pods" all hit the same GVR.
type Kind struct {
	// Name — wire identifier ("pod", "deployment", "vcluster",
	// "server.hcloud"). Matched case-insensitively against the
	// ?kinds= query parameter on the SSE endpoint.
	Name string

	// GVR — apiserver group/version/resource the informer watches.
	GVR schema.GroupVersionResource

	// Namespaced — true when the resource is namespace-scoped.
	// Cluster-scoped resources (Node, Namespace, vCluster's
	// namespace-mapped variants) are watched at the cluster level.
	Namespaced bool

	// Sensitive — true for kinds that may carry secret material in
	// their .data / .stringData / similar fields. The snapshot path
	// scrubs these before writing to disk and the SSE event encoder
	// strips them before each frame leaves the process. Today: just
	// Secret. ConfigMap data is treated as PII-adjacent and also
	// stripped (see redactObject).
	Sensitive bool
}

// DefaultKinds is the built-in registry — every Sovereign starts with
// these GVRs registered. Per ADR-0001 §5 these are the K8s objects the
// Architecture graph and List pages consume.
//
// The ordering is intentional: cluster-scoped first (Namespace, Node)
// then core/v1 namespaced, apps/v1 workloads, networking, storage,
// then Crossplane managed-resources for the cloud join, then
// vCluster.
var DefaultKinds = []Kind{
	// Cluster-scoped core/v1.
	{Name: "namespace", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}, Namespaced: false},
	{Name: "node", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "nodes"}, Namespaced: false},

	// Namespaced core/v1.
	{Name: "pod", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}, Namespaced: true},
	{Name: "service", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "services"}, Namespaced: true},
	{Name: "configmap", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "configmaps"}, Namespaced: true, Sensitive: true},
	{Name: "secret", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"}, Namespaced: true, Sensitive: true},
	{Name: "persistentvolumeclaim", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumeclaims"}, Namespaced: true},
	// PV is cluster-scoped; needed by the architecture-graph PVC→Volume.hcloud
	// bridge (PV.csi.volumeAttributes carries the hcloud volume id).
	{Name: "persistentvolume", GVR: schema.GroupVersionResource{Group: "", Version: "v1", Resource: "persistentvolumes"}, Namespaced: false},

	// Workloads (apps/v1).
	{Name: "deployment", GVR: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}, Namespaced: true},
	{Name: "statefulset", GVR: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "statefulsets"}, Namespaced: true},
	{Name: "daemonset", GVR: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}, Namespaced: true},
	// ReplicaSet — intermediate ownerRef hop on the Deployment→Pod chain.
	// The graph adapter chases this hop to attribute Pods to their Deployment.
	{Name: "replicaset", GVR: schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}, Namespaced: true},

	// Networking (networking.k8s.io/v1).
	{Name: "ingress", GVR: schema.GroupVersionResource{Group: "networking.k8s.io", Version: "v1", Resource: "ingresses"}, Namespaced: true},
	// EndpointSlice — exact Service→Pod membership without recomputing
	// label-selector matches client-side for every Service-Pod pair.
	{Name: "endpointslice", GVR: schema.GroupVersionResource{Group: "discovery.k8s.io", Version: "v1", Resource: "endpointslices"}, Namespaced: true},

	// Crossplane managed resources — provider-hcloud's K8s projection
	// of cloud-side objects (ADR-0001 §5: cloud + K8s data are
	// pre-married before reaching Catalyst).
	{Name: "server.hcloud", GVR: schema.GroupVersionResource{Group: "hcloud.crossplane.io", Version: "v1alpha1", Resource: "servers"}, Namespaced: false},
	{Name: "loadbalancer.hcloud", GVR: schema.GroupVersionResource{Group: "hcloud.crossplane.io", Version: "v1alpha1", Resource: "loadbalancers"}, Namespaced: false},
	{Name: "network.hcloud", GVR: schema.GroupVersionResource{Group: "hcloud.crossplane.io", Version: "v1alpha1", Resource: "networks"}, Namespaced: false},
	{Name: "volume.hcloud", GVR: schema.GroupVersionResource{Group: "hcloud.crossplane.io", Version: "v1alpha1", Resource: "volumes"}, Namespaced: false},

	// vCluster.io tenants.
	{Name: "vcluster", GVR: schema.GroupVersionResource{Group: "vcluster.com", Version: "v1alpha1", Resource: "vclusters"}, Namespaced: true},

	// metrics.k8s.io/PodMetrics — drives the dashboard's
	// `color_by=utilization` overlay (#1084). Earlier versions excluded
	// this kind because the synchronous AddCluster discovery probe
	// blocked startup on dead kubeconfigs. With that probe removed,
	// dynamicinformer can attempt LIST+WATCH directly — when the API
	// isn't served the informer logs a soft error and retries with
	// exponential backoff (no hot loop, no startup block). Every
	// real Sovereign ships bp-metrics-server in the platform bundle,
	// so the utilization gradient renders out of the box.
	{Name: "podmetrics", GVR: schema.GroupVersionResource{Group: "metrics.k8s.io", Version: "v1beta1", Resource: "pods"}, Namespaced: true},

	// EPIC-1 (#1096) — Compliance: Kyverno PolicyReports.
	//
	// `wgpolicyk8s.io/v1alpha2/PolicyReport` is the namespace-scoped
	// per-resource compliance report Kyverno emits for every Pod /
	// Workload it audits. `ClusterPolicyReport` is the cluster-scoped
	// equivalent for cluster-scoped resources (Namespaces, Nodes,
	// CRDs, …). The score aggregator (slice S1) consumes both via the
	// same SSE fanout the architecture graph already uses — no special
	// path. The reports themselves carry no secret material (Kyverno
	// omits the offending object's data fields by design) so
	// Sensitive=false is correct.
	{Name: "policyreport", GVR: schema.GroupVersionResource{Group: "wgpolicyk8s.io", Version: "v1alpha2", Resource: "policyreports"}, Namespaced: true},
	{Name: "clusterpolicyreport", GVR: schema.GroupVersionResource{Group: "wgpolicyk8s.io", Version: "v1alpha2", Resource: "clusterpolicyreports"}, Namespaced: false},

	// EPIC-4 Slice R4 (#1099) — Events panel feed.
	//
	// Kubernetes Events live at events.k8s.io/v1 (the modern API; the
	// legacy core/v1 Events GVR is still served but the new group is
	// canonical from K8s 1.19+). The Events panel filters Events by
	// `involvedObject.namespace` + `involvedObject.name` matching the
	// focused resource — client-side filter happens in the EventsPanel
	// widget; the server-side stream surface (existing ?fieldSelector=
	// grammar) supports metadata-level filtering today.
	{Name: "event", GVR: schema.GroupVersionResource{Group: "events.k8s.io", Version: "v1", Resource: "events"}, Namespaced: true},

	// QA-loop iter-2 Fix #17 — CRDs surfaced through /k8s/{kind} need
	// matching registry entries; otherwise the handler returns
	// `{"error":"unknown kind",...}` even when the CRD is installed
	// and the apiserver would happily serve it. Caught live on
	// omantel.biz: TC-070..075, TC-184/192/194/227 all returned the
	// "unknown kind" body for helmreleases / applications / blueprints
	// / useraccesses / organizations / environments — the kinds were
	// reachable individually via the existing per-CRD handlers
	// (HandleListUserAccesses, etc.) but the generic SSE / list surface
	// at /api/v1/sovereigns/{id}/k8s/{kind} did not know about them.
	//
	// Per feedback_chroot_in_cluster_fallback.md: every new GVR added
	// here MUST get a matching rule on catalyst-api-cutover-driver
	// ClusterRole (clusterrole-cutover-driver.yaml) — the chroot
	// SovereignClient uses that SA via in-cluster fallback.
	//
	// helm.toolkit.fluxcd.io/v2 HelmReleases — Flux's release records
	// for every blueprint installed on the cluster. The dashboard's
	// platform-health view + the components page both consume this.
	{Name: "helmrelease", GVR: schema.GroupVersionResource{Group: "helm.toolkit.fluxcd.io", Version: "v2", Resource: "helmreleases"}, Namespaced: true},
	// access.openova.io/v1alpha1 UserAccess — RBAC binding CRD
	// surfaced on the /users page.
	{Name: "useraccess", GVR: schema.GroupVersionResource{Group: "access.openova.io", Version: "v1alpha1", Resource: "useraccesses"}, Namespaced: true},
	// apps.openova.io/v1alpha1 Application — workload CRD owning the
	// `/apps` and AppDetail pages (EPIC-2 slice T+O+P).
	{Name: "application", GVR: schema.GroupVersionResource{Group: "apps.openova.io", Version: "v1alpha1", Resource: "applications"}, Namespaced: true},
	// catalyst.openova.io/v1alpha1 Blueprint — published blueprint
	// records the curate/publish handlers operate on.
	{Name: "blueprint", GVR: schema.GroupVersionResource{Group: "catalyst.openova.io", Version: "v1alpha1", Resource: "blueprints"}, Namespaced: true},
	// orgs.openova.io/v1alpha1 Organization — top-level tenancy CRD
	// surfaced on the /organizations page.
	{Name: "organization", GVR: schema.GroupVersionResource{Group: "orgs.openova.io", Version: "v1alpha1", Resource: "organizations"}, Namespaced: false},
	// catalyst.openova.io/v1alpha1 Environment — logical environment
	// dimension (one Environment realised by N Clusters per
	// docs/NAMING-CONVENTION.md). Surfaced on the /environments page.
	{Name: "environment", GVR: schema.GroupVersionResource{Group: "catalyst.openova.io", Version: "v1alpha1", Resource: "environments"}, Namespaced: true},
}

// Registry is a runtime-mutable lookup keyed by the short Name. It
// fronts every read path: the REST list handler resolves
// path-segment → Kind → GVR, the SSE handler resolves ?kinds=foo,bar
// → []Kind, and the factory iterates over Registry.All() to spawn
// informers.
//
// Two indexes are maintained internally:
//
//   - byName    — canonical singular short-name (the wire identifier).
//   - byPlural  — alias index covering the GVR.Resource (plural form
//     emitted by `kubectl api-resources`) plus a small set of
//     common short-name aliases (pvc → persistentvolumeclaim,
//     pv → persistentvolume, ns → namespace, …). Handlers
//     accept any of these forms transparently.
//
// `kubectl get pods` and `kubectl get pod` both work; the catalyst-api
// REST list endpoint must mirror that ergonomic. Without the plural
// index a path like `/k8s/services` returned 404 even though
// `/k8s/service` succeeded — surprising for operators with kubectl
// muscle memory and surfaced in the iter-1 QA loop matrix as TC-084 /
// TC-085 / TC-090 / TC-091 / TC-130 (cloud-list 404s on plural paths).
type Registry struct {
	byName   map[string]Kind
	byPlural map[string]string // alias → canonical singular Name
}

// kindShortAliases maps common kubectl short-names to the canonical
// singular Name a Kind is registered under. Mirrors the conventional
// `kubectl api-resources -o wide` short-name column for the few cases
// where the short form is in widespread operator muscle memory and
// not derivable from the GVR.Resource (the simple "trim trailing s"
// rule the plural index handles).
var kindShortAliases = map[string]string{
	"ns":     "namespace",
	"no":     "node",
	"po":     "pod",
	"svc":    "service",
	"cm":     "configmap",
	"sec":    "secret", // not a real kubectl alias but unambiguous
	"pvc":    "persistentvolumeclaim",
	"pvcs":   "persistentvolumeclaim", // pluralised short form (matrix usage)
	"pv":     "persistentvolume",
	"pvs":    "persistentvolume",
	"deploy": "deployment",
	"sts":    "statefulset",
	"ds":     "daemonset",
	"rs":     "replicaset",
	"ing":    "ingress",
	"ep":     "endpointslice",
	"ev":     "event",
}

// NewRegistry — start empty; callers pass DefaultKinds (or the loaded
// ConfigMap registry) to Add to build the runtime set.
func NewRegistry() *Registry {
	return &Registry{
		byName:   map[string]Kind{},
		byPlural: map[string]string{},
	}
}

// Add or replace a Kind in the registry. Name is canonicalised
// lower-case so the ?kinds=Pod query path resolves correctly.
//
// Two index entries are written:
//
//  1. byName[canonical(k.Name)] = k         — singular wire identifier
//  2. byPlural[canonical(k.GVR.Resource)]   — kubectl plural form
//
// Returns an error when GVR.Resource is empty — every registered Kind
// must specify the resource (the GVR's plural form). Group + Version
// may be empty for core/v1 resources.
func (r *Registry) Add(k Kind) error {
	if k.Name == "" {
		return fmt.Errorf("k8scache: Kind.Name is required")
	}
	if k.GVR.Resource == "" {
		return fmt.Errorf("k8scache: Kind.GVR.Resource is required for %q", k.Name)
	}
	canonName := canonicalKindName(k.Name)
	r.byName[canonName] = k
	// Maintain the plural alias index. Two collision rules:
	//
	//   (1) If the plural collides with a registered singular Name
	//       (e.g. someone registered Name="services"), the singular wins
	//       and the alias is skipped.
	//   (2) If the plural is already aliased to a DIFFERENT singular
	//       (concrete: metrics.k8s.io/PodMetrics has GVR.Resource="pods"
	//       same as core/v1 Pod's plural), the FIRST registration wins.
	//       Without this guard, registration order silently flips which
	//       Kind `/k8s/pods` resolves to, depending on whether pod or
	//       podmetrics was loaded first.
	plural := canonicalKindName(k.GVR.Resource)
	if plural != "" && plural != canonName {
		if _, takenSingular := r.byName[plural]; !takenSingular {
			if _, alreadyAliased := r.byPlural[plural]; !alreadyAliased {
				r.byPlural[plural] = canonName
			}
		}
	}
	return nil
}

// Get returns the Kind for a name (case-insensitive). Resolution order:
//
//  1. Exact canonical match against the registered singular Name.
//  2. Plural alias (matches against GVR.Resource — the form `kubectl
//     api-resources` prints, e.g. "services", "namespaces", "pods").
//  3. Common short alias (pvc, ns, svc, …) — see kindShortAliases.
//
// Returns false when none of the three resolve — handlers translate
// that to a 404 with the kinds list.
func (r *Registry) Get(name string) (Kind, bool) {
	canon := canonicalKindName(name)
	if k, ok := r.byName[canon]; ok {
		return k, true
	}
	if singular, ok := r.byPlural[canon]; ok {
		if k, ok := r.byName[singular]; ok {
			return k, true
		}
	}
	if singular, ok := kindShortAliases[canon]; ok {
		if k, ok := r.byName[singular]; ok {
			return k, true
		}
	}
	return Kind{}, false
}

// All returns every registered Kind. Allocation-light — callers iterate
// once at factory boot to spawn informers, and again on each SSE
// connection to validate ?kinds=. Stable iteration order is NOT
// guaranteed (Go map ranging is randomised); the SSE multiplexer
// sorts by Name on connect.
func (r *Registry) All() []Kind {
	out := make([]Kind, 0, len(r.byName))
	for _, k := range r.byName {
		out = append(out, k)
	}
	return out
}

// Names returns every registered Kind name in lower-case
// canonicalised form. Used by the REST 404 path so an operator who
// types `/k8s/podz` gets back a list of valid kinds.
func (r *Registry) Names() []string {
	out := make([]string, 0, len(r.byName))
	for n := range r.byName {
		out = append(out, n)
	}
	return out
}

// canonicalKindName lowercases the input and trims surrounding
// whitespace. Plural / singular are NOT collapsed — the registry
// stores whatever form was registered. Handlers normalise the inbound
// query parameter via this same function.
func canonicalKindName(s string) string {
	out := make([]byte, 0, len(s))
	for i := 0; i < len(s); i++ {
		c := s[i]
		if c == ' ' || c == '\t' || c == '\n' || c == '\r' {
			continue
		}
		if c >= 'A' && c <= 'Z' {
			c += 'a' - 'A'
		}
		out = append(out, c)
	}
	return string(out)
}

// CanonicalKindName exposes the canonicalisation rule for handlers
// that need to compare ?kinds= input against registered names.
func CanonicalKindName(s string) string { return canonicalKindName(s) }
