#!/usr/bin/env bash
#
# export-k8s-manifests.sh
#
# Ricostruisce i manifest YAML dichiarativi delle risorse installate
# su un cluster Kubernetes, per una lista di namespace data in input.
#
# Rimuove i campi "runtime" (status, managedFields, resourceVersion, uid,
# creationTimestamp, ownerReferences generate da controller, ecc.) in modo
# da produrre YAML riapplicabili con `kubectl apply -f`.
#
# Uso:
#   ./export-k8s-manifests.sh -n ns1,ns2,ns3 [-o output_dir] [-c kubeconfig] [-k context]
#   ./export-k8s-manifests.sh -f namespaces.txt [-o output_dir]
#
# Dipendenze:
#   - kubectl (obbligatorio)
#   - yq (v4+, https://github.com/mikefarah/yq) — consigliato per pulizia campi
#     Se assente, viene usato un fallback con python3.
#
set -euo pipefail

# ------------------------------------------------------------------
# Default
# ------------------------------------------------------------------
OUTPUT_DIR="./k8s-export-$(date +%Y%m%d-%H%M%S)"
NAMESPACES=()
NAMESPACE_FILE=""
KUBECONFIG_OPT=()
CONTEXT_OPT=()
INCLUDE_SECRETS=false

# Tipi di risorsa namespaced da esportare.
# Personalizza questa lista secondo le tue esigenze.
RESOURCE_TYPES=(
  configmap
  secret
  service
  serviceaccount
  deployment
  statefulset
  daemonset
  replicaset
  job
  cronjob
  ingress
  networkpolicy
  poddisruptionbudget
  horizontalpodautoscaler
  persistentvolumeclaim
  role
  rolebinding

  # --- API Gateway (namespaced) -------------------------------------
  # Gateway API standard (gateway.networking.k8s.io)
  gateway
  httproute
  grpcroute
  tcproute
  tlsroute
  referencegrant
  # Istio
  virtualservice
  destinationrule
  # Traefik
  ingressroute
  middleware
  # Kong
  kongingress
  kongplugin
  kongconsumer
  # APISIX
  apisixroute
  apisixpluginconfig
  # NGINX Gateway Fabric — CRD di estensione (gateway.nginx.org)
  # Le risorse Gateway API standard (gateway, httproute, gatewayclass, ecc.)
  # sono già coperte sopra; queste sono le estensioni specifiche NGF per
  # configurazioni non esprimibili nello standard Gateway API.
  nginxproxy
  clientsettingspolicy
  observabilitypolicy
  upstreamsettingspolicy
  snippetsfilter

  # CRD comuni — decommenta/aggiungi secondo gli operator installati
  cluster.postgresql.cnpg.io        # CloudNativePG Cluster
  scheduledbackup.postgresql.cnpg.io
  certificate.cert-manager.io
)

# Tipi di risorsa CLUSTER-SCOPED da esportare.
# Vengono filtrate per pertinenza ai namespace indicati dove possibile
# (es. ClusterRoleBinding con subject in uno dei namespace, PV con claim
# in uno dei namespace). Le risorse "globali" (StorageClass, GatewayClass,
# IngressClass, PriorityClass) vengono esportate sempre, essendo poche
# e prive di legame diretto con un namespace.
CLUSTER_RESOURCE_TYPES_FILTERED=(
  clusterrolebinding
  persistentvolume
)
CLUSTER_RESOURCE_TYPES_GLOBAL=(
  storageclass
  priorityclass
  # API Gateway (cluster-scoped)
  gatewayclass
  ingressclass
  # Da valutare se esportare sempre (potenzialmente numerose/rumorose):
  # clusterrole
  # customresourcedefinition
)

# ------------------------------------------------------------------
# Parsing argomenti
# ------------------------------------------------------------------
usage() {
  cat <<EOF
Uso: $0 -n ns1,ns2,ns3 [opzioni]
     $0 -f namespaces.txt [opzioni]

Opzioni:
  -n <ns1,ns2,...>   Lista namespace separati da virgola
  -f <file>          File con un namespace per riga (alternativo a -n)
  -o <dir>           Directory di output (default: ${OUTPUT_DIR})
  -c <kubeconfig>    Path al kubeconfig da usare
  -k <context>       Nome del context kubectl da usare
  -s                 Includi anche i Secret (default: esclusi per sicurezza)
  -h                 Mostra questo help
EOF
  exit 1
}

while getopts "n:f:o:c:k:sh" opt; do
  case "$opt" in
    n) IFS=',' read -r -a NAMESPACES <<< "$OPTARG" ;;
    f) NAMESPACE_FILE="$OPTARG" ;;
    o) OUTPUT_DIR="$OPTARG" ;;
    c) KUBECONFIG_OPT=(--kubeconfig "$OPTARG") ;;
    k) CONTEXT_OPT=(--context "$OPTARG") ;;
    s) INCLUDE_SECRETS=true ;;
    h) usage ;;
    *) usage ;;
  esac
done

if [[ -n "$NAMESPACE_FILE" ]]; then
  while IFS= read -r line; do
    [[ -z "$line" || "$line" =~ ^# ]] && continue
    NAMESPACES+=("$line")
  done < "$NAMESPACE_FILE"
fi

if [[ ${#NAMESPACES[@]} -eq 0 ]]; then
  echo "Errore: nessun namespace specificato (usa -n o -f)" >&2
  usage
fi

if [[ "$INCLUDE_SECRETS" == false ]]; then
  RESOURCE_TYPES=("${RESOURCE_TYPES[@]/secret}")
fi

KCTL=(kubectl "${KUBECONFIG_OPT[@]}" "${CONTEXT_OPT[@]}")

# ------------------------------------------------------------------
# Verifica dipendenze
# ------------------------------------------------------------------
command -v kubectl >/dev/null 2>&1 || { echo "Errore: kubectl non trovato" >&2; exit 1; }

USE_YQ=false
if command -v yq >/dev/null 2>&1; then
  USE_YQ=true
elif command -v python3 >/dev/null 2>&1; then
  USE_YQ=false
else
  echo "Errore: serve 'yq' oppure 'python3' per la pulizia dei manifest" >&2
  exit 1
fi

echo "Verifica connessione al cluster..."
"${KCTL[@]}" cluster-info >/dev/null 2>&1 || { echo "Errore: impossibile contattare il cluster" >&2; exit 1; }
CLUSTER_NAME=$("${KCTL[@]}" config current-context 2>/dev/null || echo "unknown-cluster")
echo "Cluster context: ${CLUSTER_NAME}"
echo "Output dir: ${OUTPUT_DIR}"
echo "Namespace da esportare: ${NAMESPACES[*]}"
echo "Secret inclusi: ${INCLUDE_SECRETS}"
echo

mkdir -p "$OUTPUT_DIR"

# Gli script Python vengono scritti su file temporanei perché in bash
# `python3 - <<'HEREDOC'` sovrascrive lo stdin del processo con l'heredoc,
# rendendo impossibile leggere i dati dalla pipeline (il fd 0 è già consumato
# prima che il codice Python arrivi a sys.stdin.read()).
_PY_CLEAN=$(mktemp)
_PY_FILTER=$(mktemp)
trap 'rm -f "$_PY_CLEAN" "$_PY_FILTER"' EXIT

if [[ "$USE_YQ" == false ]]; then
  cat > "$_PY_CLEAN" << 'PYCLEAN'
import sys, yaml
doc = yaml.safe_load(sys.stdin)
if doc is None:
    sys.exit(0)
doc.pop("status", None)
md = doc.get("metadata", {})
for f in ("uid", "resourceVersion", "generation", "creationTimestamp",
          "selfLink", "managedFields", "ownerReferences"):
    md.pop(f, None)
ann = md.get("annotations")
if ann:
    ann.pop("kubectl.kubernetes.io/last-applied-configuration", None)
    ann.pop("deployment.kubernetes.io/revision", None)
    if not ann:
        md.pop("annotations", None)
spec = doc.get("spec", {})
spec.pop("clusterIP", None)
spec.pop("clusterIPs", None)
tmpl = spec.get("template", {})
if isinstance(tmpl, dict):
    tmpl.get("metadata", {}).pop("creationTimestamp", None)
print(yaml.dump(doc, sort_keys=False, default_flow_style=False), end="")
PYCLEAN

  cat > "$_PY_FILTER" << 'PYFILTER'
import json, os, sys
mode = sys.argv[1]
data = json.load(sys.stdin)
ns_set = set(os.environ.get("NS_CSV", "").split())
for item in data.get("items", []):
    if mode == "subjects":
        subs = item.get("subjects") or []
        match = any(s.get("namespace") in ns_set for s in subs)
    else:
        claim = (item.get("spec") or {}).get("claimRef") or {}
        match = claim.get("namespace") in ns_set
    if match:
        print(item["metadata"]["name"])
PYFILTER
fi

# ------------------------------------------------------------------
# Funzione di pulizia dei campi runtime
# ------------------------------------------------------------------
clean_manifest_yq() {
  # Rimuove campi generati dal cluster, mantenendo lo spec dichiarativo
  yq eval '
    del(.status) |
    del(.metadata.uid) |
    del(.metadata.resourceVersion) |
    del(.metadata.generation) |
    del(.metadata.creationTimestamp) |
    del(.metadata.selfLink) |
    del(.metadata.managedFields) |
    del(.metadata.annotations."kubectl.kubernetes.io/last-applied-configuration") |
    del(.metadata.annotations."deployment.kubernetes.io/revision") |
    del(.metadata.ownerReferences) |
    del(.spec.clusterIP) |
    del(.spec.clusterIPs) |
    del(.spec.template.metadata.creationTimestamp)
  ' -
}

clean_manifest_python() {
  # Usa il file temporaneo creato all'avvio; l'heredoc inline non funziona
  # in pipeline perché sovrascrive lo stdin del processo.
  python3 "$_PY_CLEAN"
}

clean_manifest() {
  if [[ "$USE_YQ" == true ]]; then
    clean_manifest_yq
  else
    clean_manifest_python
  fi
}

# ------------------------------------------------------------------
# Export per namespace / kind / risorsa
# ------------------------------------------------------------------
SUMMARY_FILE="${OUTPUT_DIR}/EXPORT_SUMMARY.md"
{
  echo "# Export manifest Kubernetes"
  echo
  echo "- Cluster context: \`${CLUSTER_NAME}\`"
  echo "- Data export: $(date -u +'%Y-%m-%dT%H:%M:%SZ')"
  echo "- Namespace: ${NAMESPACES[*]}"
  echo
  echo "| Namespace | Kind | Nome | File |"
  echo "|---|---|---|---|"
} > "$SUMMARY_FILE"

for ns in "${NAMESPACES[@]}"; do
  echo "== Namespace: ${ns} =="

  if ! "${KCTL[@]}" get namespace "$ns" >/dev/null 2>&1; then
    echo "  ATTENZIONE: namespace '${ns}' non trovato, salto." >&2
    continue
  fi

  NS_DIR="${OUTPUT_DIR}/${ns}"
  mkdir -p "$NS_DIR"

  for kind in "${RESOURCE_TYPES[@]}"; do
    [[ -z "$kind" ]] && continue

    # Il controllo api-resources è omesso: kubectl get <kind> 2>/dev/null || true
    # gestisce già i resource type non esistenti restituendo NAMES vuoto.
    NAMES=$("${KCTL[@]}" -n "$ns" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
    [[ -z "$NAMES" ]] && continue

    KIND_DIR="${NS_DIR}/${kind}"
    mkdir -p "$KIND_DIR"

    while IFS= read -r name; do
      [[ -z "$name" ]] && continue

      # Salta le risorse generate automaticamente (es. default token secret,
      # ReplicaSet gestiti da Deployment, Pod gestiti da controller superiori)
      if [[ "$kind" == "secret" && "$name" =~ ^default-token- ]]; then
        continue
      fi

      OUT_FILE="${KIND_DIR}/${name}.yaml"
      echo "  -> ${kind}/${name}"

      if "${KCTL[@]}" -n "$ns" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$OUT_FILE"; then
        echo "| ${ns} | ${kind} | ${name} | \`${ns}/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE"
      else
        echo "     ATTENZIONE: export fallito per ${kind}/${name}" >&2
        rm -f "$OUT_FILE"
      fi
    done <<< "$NAMES"
  done
done

# ------------------------------------------------------------------
# Export risorse cluster-scoped
# ------------------------------------------------------------------
echo
echo "== Risorse cluster-scoped =="

CLUSTER_DIR="${OUTPUT_DIR}/_cluster-scoped"

# Cache unica delle risorse disponibili (evita N chiamate a api-resources)
_AVAIL_NAMES=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($1)}' || true)
_AVAIL_KINDS=$("${KCTL[@]}" api-resources --no-headers 2>/dev/null | awk '{print tolower($NF)}' || true)

resource_type_exists() {
  local kind="$1"
  # Controlla sia la colonna NAME (plurale) sia la colonna KIND (singolare) del
  # risultato di api-resources per gestire plurali irregolari (es. ingress→ingresses).
  echo "$_AVAIL_NAMES" | grep -qx "${kind}s" \
    || echo "$_AVAIL_KINDS" | grep -qx "$kind"
}

export_cluster_resource() {
  local kind="$1"
  local name="$2"
  local kind_dir="${CLUSTER_DIR}/${kind}"
  mkdir -p "$kind_dir"
  local out_file="${kind_dir}/${name}.yaml"

  echo "  -> ${kind}/${name}"
  if "${KCTL[@]}" get "$kind" "$name" -o yaml 2>/dev/null | clean_manifest > "$out_file"; then
    echo "| (cluster) | ${kind} | ${name} | \`_cluster-scoped/${kind}/${name}.yaml\` |" >> "$SUMMARY_FILE"
  else
    echo "     ATTENZIONE: export fallito per ${kind}/${name}" >&2
    rm -f "$out_file"
  fi
}

# --- Risorse globali: esportate sempre, senza filtro namespace ---
for kind in "${CLUSTER_RESOURCE_TYPES_GLOBAL[@]}"; do
  [[ -z "$kind" ]] && continue
  resource_type_exists "$kind" || continue

  NAMES=$("${KCTL[@]}" get "$kind" -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' 2>/dev/null || true)
  [[ -z "$NAMES" ]] && continue

  while IFS= read -r name; do
    [[ -z "$name" ]] && continue
    export_cluster_resource "$kind" "$name"
  done <<< "$NAMES"
done

# Filtra gli item di un array JSON kubectl (-o json) tenendo solo quelli
# per cui `jq_path` (letto tramite kubectl jsonpath-like con yq/python) ha
# un valore presente nella lista NAMESPACES. Stampa i nomi corrispondenti.
# $1 = json completo (stdin)  $2 = "subjects" | "claimref"
filter_by_namespace_field() {
  local mode="$1"
  if [[ "$USE_YQ" == true ]]; then
    if [[ "$mode" == "subjects" ]]; then
      yq eval '.items[] | select((.subjects // []) | any_c(.namespace == env(NS_MATCH))) | .metadata.name' -
    else
      yq eval '.items[] | select(.spec.claimRef.namespace == env(NS_MATCH)) | .metadata.name' -
    fi
  else
    python3 "$_PY_FILTER" "$mode"
  fi
}

# --- ClusterRoleBinding: solo quelli con subject in uno dei namespace indicati ---
if resource_type_exists clusterrolebinding; then
  MATCHED_CRB=""
  for ns in "${NAMESPACES[@]}"; do
    RESULT=$("${KCTL[@]}" get clusterrolebinding -o json 2>/dev/null \
      | NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field subjects || true)
    MATCHED_CRB="${MATCHED_CRB}
${RESULT}"
  done
  MATCHED_CRB=$(echo "$MATCHED_CRB" | sort -u | sed '/^$/d')

  while IFS= read -r name; do
    [[ -z "$name" ]] && continue
    export_cluster_resource clusterrolebinding "$name"
  done <<< "$MATCHED_CRB"
fi

# --- PersistentVolume: solo quelli con claimRef in uno dei namespace indicati ---
if resource_type_exists persistentvolume; then
  MATCHED_PV=""
  for ns in "${NAMESPACES[@]}"; do
    RESULT=$("${KCTL[@]}" get persistentvolume -o json 2>/dev/null \
      | NS_CSV="${NAMESPACES[*]}" NS_MATCH="$ns" filter_by_namespace_field claimref || true)
    MATCHED_PV="${MATCHED_PV}
${RESULT}"
  done
  MATCHED_PV=$(echo "$MATCHED_PV" | sort -u | sed '/^$/d')

  while IFS= read -r name; do
    [[ -z "$name" ]] && continue
    export_cluster_resource persistentvolume "$name"
  done <<< "$MATCHED_PV"
fi

echo
echo "Export completato in: ${OUTPUT_DIR}"
echo "Riepilogo: ${SUMMARY_FILE}"