// Project:   scalo
// File:      src/deployment/generate/helm.rs
// Purpose:   Helm chart generation (Chart.yaml, values, templates)
// Language:  Rust
//
// License:   Apache-2.0
// Copyright: (c) 2026 HYPERI PTY LIMITED

#![allow(clippy::format_push_string)]

use std::path::Path;

use crate::deployment::contract::DeploymentContract;
use crate::deployment::error::DeploymentError;

use super::common::{safe_template_lookup, to_camel_suffix, write_file};

// ============================================================================
// Helm chart
// ============================================================================

/// Generate a complete Helm chart directory from the deployment contract.
///
/// Writes `Chart.yaml`, `values.yaml`, and all template files to `output_dir`.
///
/// `identity`, when provided, stamps the three `io.hyperi.contract.*`
/// annotations into `Chart.yaml`'s top-level `annotations:` block per the
/// Contract Identity Annotation Scheme v1. Phase 1 rollout: optional;
/// callers SHOULD pass `Some(&identity)`.
///
/// # Errors
///
/// Returns `DeploymentError` if files or directories cannot be created.
pub fn generate_chart(
    contract: &DeploymentContract,
    output_dir: impl AsRef<Path>,
    identity: Option<&crate::deployment::ContractIdentity>,
) -> Result<(), DeploymentError> {
    let dir = output_dir.as_ref();
    let templates_dir = dir.join("templates");

    // Create directories
    std::fs::create_dir_all(&templates_dir).map_err(|e| DeploymentError::CreateDir {
        path: templates_dir.display().to_string(),
        source: e,
    })?;

    // Write all chart files
    write_file(dir.join("Chart.yaml"), &gen_chart_yaml(contract, identity))?;
    write_file(dir.join("values.yaml"), &gen_values_yaml(contract))?;
    write_file(
        templates_dir.join("_helpers.tpl"),
        &gen_helpers_tpl(contract),
    )?;
    write_file(
        templates_dir.join("deployment.yaml"),
        &gen_deployment_yaml(contract),
    )?;
    write_file(
        templates_dir.join("service.yaml"),
        &gen_service_yaml(contract),
    )?;
    write_file(
        templates_dir.join("serviceaccount.yaml"),
        &gen_serviceaccount_yaml(contract),
    )?;
    write_file(
        templates_dir.join("configmap.yaml"),
        &gen_configmap_yaml(contract),
    )?;
    write_file(
        templates_dir.join("secret.yaml"),
        &gen_secret_yaml(contract),
    )?;
    write_file(templates_dir.join("hpa.yaml"), &gen_hpa_yaml(contract))?;

    if contract.keda.is_some() {
        write_file(
            templates_dir.join("keda-scaledobject.yaml"),
            &gen_keda_scaledobject_yaml(contract),
        )?;
        write_file(
            templates_dir.join("keda-triggerauth.yaml"),
            &gen_keda_triggerauth_yaml(contract),
        )?;
    }

    write_file(templates_dir.join("NOTES.txt"), &gen_notes_txt(contract))?;

    Ok(())
}

// ============================================================================
// Chart file generators
// ============================================================================

fn gen_chart_yaml(
    c: &DeploymentContract,
    identity: Option<&crate::deployment::ContractIdentity>,
) -> String {
    // Contract Identity Annotation Scheme v1 -- top-level annotations block.
    let identity_block = identity
        .map(|id| format!("\nannotations:\n{ann}\n", ann = id.as_yaml_annotations(2)))
        .unwrap_or_default();

    format!(
        "apiVersion: v2\n\
         name: {name}\n\
         description: {desc}\n\
         type: application\n\
         version: 0.1.0\n\
         appVersion: \"1.0.0\"\n\
         {identity_block}\n\
         keywords:\n\
         \x20 - {name}\n",
        name = c.app_name,
        desc = if c.description.is_empty() {
            &c.app_name
        } else {
            &c.description
        },
        identity_block = identity_block,
    )
}

#[allow(clippy::too_many_lines)]
fn gen_values_yaml(c: &DeploymentContract) -> String {
    let mut out = String::with_capacity(2048);

    // Header comment
    out.push_str(&format!(
        "# {app} Helm chart values\n\
         #\n\
         # Generated by scalo deployment module.\n\
         # Contract points validated by cargo test.\n\
         \n",
        app = c.app_name,
    ));

    // Replicas, image, overrides
    out.push_str(&format!(
        "# -- Number of replicas (ignored when KEDA is enabled)\n\
         replicaCount: 1\n\
         \n\
         image:\n\
         \x20 repository: {registry}/{app}\n\
         \x20 # -- Defaults to Chart appVersion\n\
         \x20 tag: \"\"\n\
         \x20 pullPolicy: IfNotPresent\n\
         \n\
         imagePullSecrets: []\n\
         nameOverride: \"\"\n\
         fullnameOverride: \"\"\n\
         \n",
        registry = c.image_registry,
        app = c.app_name,
    ));

    // Service account
    out.push_str(
        "serviceAccount:\n\
         \x20 create: true\n\
         \x20 annotations: {}\n\
         \x20 # -- If not set, name is generated from fullname\n\
         \x20 name: \"\"\n\
         \n",
    );

    // Pod annotations (Prometheus)
    out.push_str(&format!(
        "# -- Pod annotations (Prometheus scrape config included by default)\n\
         podAnnotations:\n\
         \x20 prometheus.io/scrape: \"true\"\n\
         \x20 prometheus.io/port: \"{port}\"\n\
         \x20 prometheus.io/path: \"{metrics_path}\"\n\
         \n\
         podLabels: {{}}\n\
         \n",
        port = c.metrics_port,
        metrics_path = c.health.metrics_path,
    ));

    // Resources
    out.push_str(
        "resources:\n\
         \x20 requests:\n\
         \x20   cpu: 250m\n\
         \x20   memory: 256Mi\n\
         \x20 limits:\n\
         \x20   cpu: \"2\"\n\
         \x20   memory: 1Gi\n\
         \n",
    );

    // Service
    out.push_str(&format!(
        "# -- Metrics and health endpoint service\n\
         service:\n\
         \x20 type: ClusterIP\n\
         \x20 port: {port}\n\
         \n",
        port = c.metrics_port,
    ));

    // App config section
    out.push_str(&format!(
        "# -- Application configuration (mounted as {})\n",
        c.config_mount_path
    ));
    if let Some(ref config) = c.default_config {
        out.push_str("config:\n");
        // Serialise the config value as YAML and indent by 2
        if let Ok(yaml) = serde_yaml_ng::to_string(config) {
            for line in yaml.lines() {
                if line == "---" {
                    continue;
                }
                out.push_str(&format!("  {line}\n"));
            }
        }
    } else {
        out.push_str("config: {}\n");
    }
    out.push('\n');

    // Secret sections
    for group in &c.secrets {
        out.push_str(&format!(
            "# -- {} credentials\n\
             {}:\n\
             \x20 existingSecret: \"\"\n\
             \x20 secretKeys:\n",
            group.group_name, group.group_name,
        ));
        for env in &group.env_vars {
            out.push_str(&format!("    {}: {}\n", env.key_name, env.secret_key));
        }
        for env in &group.env_vars {
            out.push_str(&format!("  {}: \"\"\n", env.key_name));
        }
        out.push('\n');
    }

    // KEDA section. Always emit a `keda:` block in values.yaml even when
    // the contract has no KEDA config -- templates reference
    // `.Values.keda.enabled` unconditionally, so the key must exist or
    // `helm lint` panics with "nil pointer evaluating interface
    // {}.enabled". When the contract opts out, the block is just
    // `enabled: false`.
    if let Some(ref keda) = c.keda {
        out.push_str(&format!(
            "# -- KEDA autoscaling (requires KEDA operator installed)\n\
             keda:\n\
             \x20 enabled: true\n\
             \x20 minReplicaCount: {min}\n\
             \x20 maxReplicaCount: {max}\n\
             \x20 pollingInterval: {poll}\n\
             \x20 cooldownPeriod: {cool}\n\
             \x20 kafka:\n\
             \x20   # -- Scale when consumer group lag exceeds this per partition\n\
             \x20   lagThreshold: \"{lag}\"\n\
             \x20   # -- Wake from zero replicas when lag exceeds this\n\
             \x20   activationLagThreshold: \"{activation}\"\n\
             \x20   # -- Override topic (default: first topic from config)\n\
             \x20   topic: \"\"\n\
             \x20   # -- Override consumer group (default: from config)\n\
             \x20   consumerGroup: \"\"\n\
             \x20 cpu:\n\
             \x20   enabled: {cpu_enabled}\n\
             \x20   # -- CPU utilisation percentage threshold\n\
             \x20   threshold: \"{cpu_threshold}\"\n\
             \n",
            min = keda.min_replicas,
            max = keda.max_replicas,
            poll = keda.polling_interval,
            cool = keda.cooldown_period,
            lag = keda.kafka_lag_threshold,
            activation = keda.activation_lag_threshold,
            cpu_enabled = keda.cpu_enabled,
            cpu_threshold = keda.cpu_threshold,
        ));
    } else {
        out.push_str(
            "# -- KEDA autoscaling disabled by contract; HPA fallback below.\n\
             keda:\n\
             \x20 enabled: false\n\
             \n",
        );
    }

    // HPA fallback
    out.push_str(
        "# -- Standard HPA fallback (when KEDA is not installed)\n\
         # Mutually exclusive with keda.enabled\n\
         autoscaling:\n\
         \x20 enabled: false\n\
         \x20 minReplicas: 1\n\
         \x20 maxReplicas: 10\n\
         \x20 targetCPUUtilizationPercentage: 80\n\
         \n\
         nodeSelector: {}\n\
         tolerations: []\n\
         affinity: {}\n",
    );

    out
}

fn gen_helpers_tpl(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    let mut out = String::with_capacity(2048);

    // Standard helpers
    out.push_str(&format!(
        r#"{{{{/*
Expand the name of the chart.
*/}}}}
{{{{- define "{app}.name" -}}}}
{{{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}}}
{{{{- end }}}}

{{{{/*
Create a default fully qualified app name.
Truncated at 63 chars because some K8s name fields are limited.
*/}}}}
{{{{- define "{app}.fullname" -}}}}
{{{{- if .Values.fullnameOverride }}}}
{{{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}}}
{{{{- else }}}}
{{{{- $name := default .Chart.Name .Values.nameOverride }}}}
{{{{- if contains $name .Release.Name }}}}
{{{{- .Release.Name | trunc 63 | trimSuffix "-" }}}}
{{{{- else }}}}
{{{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}}}
{{{{- end }}}}
{{{{- end }}}}
{{{{- end }}}}

{{{{/*
Create chart name and version as used by the chart label.
*/}}}}
{{{{- define "{app}.chart" -}}}}
{{{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}}}
{{{{- end }}}}

{{{{/*
Common labels.
*/}}}}
{{{{- define "{app}.labels" -}}}}
helm.sh/chart: {{{{ include "{app}.chart" . }}}}
{{{{ include "{app}.selectorLabels" . }}}}
{{{{- if .Chart.AppVersion }}}}
app.kubernetes.io/version: {{{{ .Chart.AppVersion | quote }}}}
{{{{- end }}}}
app.kubernetes.io/managed-by: {{{{ .Release.Service }}}}
{{{{- end }}}}

{{{{/*
Selector labels.
*/}}}}
{{{{- define "{app}.selectorLabels" -}}}}
app.kubernetes.io/name: {{{{ include "{app}.name" . }}}}
app.kubernetes.io/instance: {{{{ .Release.Name }}}}
{{{{- end }}}}

{{{{/*
Service account name.
*/}}}}
{{{{- define "{app}.serviceAccountName" -}}}}
{{{{- if .Values.serviceAccount.create }}}}
{{{{- default (include "{app}.fullname" .) .Values.serviceAccount.name }}}}
{{{{- else }}}}
{{{{- default "default" .Values.serviceAccount.name }}}}
{{{{- end }}}}
{{{{- end }}}}
"#,
    ));

    // Secret name helpers -- one per secret group
    for group in &c.secrets {
        let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));
        out.push_str(&format!(
            r#"
{{{{/*
{group} secret name -- use existing or generate from fullname.
*/}}}}
{{{{- define "{app}.{helper}" -}}}}
{{{{- if .Values.{group}.existingSecret }}}}
{{{{- .Values.{group}.existingSecret }}}}
{{{{- else }}}}
{{{{- printf "%s-{group}" (include "{app}.fullname" .) }}}}
{{{{- end }}}}
{{{{- end }}}}
"#,
            app = app,
            group = group.group_name,
            helper = helper_name,
        ));
    }

    out
}

fn gen_deployment_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    let mut out = String::with_capacity(4096);

    // Header
    out.push_str(&format!(
        r#"apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{{{ include "{app}.fullname" . }}}}
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
spec:
  {{{{- if not (or .Values.keda.enabled .Values.autoscaling.enabled) }}}}
  replicas: {{{{ .Values.replicaCount }}}}
  {{{{- end }}}}
  selector:
    matchLabels:
      {{{{- include "{app}.selectorLabels" . | nindent 6 }}}}
  template:
    metadata:
      annotations:
        checksum/config: {{{{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}}}
        {{{{- with .Values.podAnnotations }}}}
        {{{{- toYaml . | nindent 8 }}}}
        {{{{- end }}}}
      labels:
        {{{{- include "{app}.labels" . | nindent 8 }}}}
        {{{{- with .Values.podLabels }}}}
        {{{{- toYaml . | nindent 8 }}}}
        {{{{- end }}}}
    spec:
      {{{{- with .Values.imagePullSecrets }}}}
      imagePullSecrets:
        {{{{- toYaml . | nindent 8 }}}}
      {{{{- end }}}}
      serviceAccountName: {{{{ include "{app}.serviceAccountName" . }}}}
      containers:
        - name: {{{{ .Chart.Name }}}}
          image: "{{{{ .Values.image.repository }}}}:{{{{ .Values.image.tag | default .Chart.AppVersion }}}}"
          imagePullPolicy: {{{{ .Values.image.pullPolicy }}}}
"#,
    ));

    // Args
    if !c.entrypoint_args.is_empty() {
        out.push_str("          args:\n");
        for arg in &c.entrypoint_args {
            out.push_str(&format!("            - \"{arg}\"\n"));
        }
    }

    // Ports
    out.push_str(
        "          ports:\n\
         \x20           - name: metrics\n\
         \x20             containerPort: {{ .Values.service.port }}\n\
         \x20             protocol: TCP\n",
    );
    for port in &c.extra_ports {
        out.push_str(&format!(
            "            - name: {name}\n\
             \x20             containerPort: {port}\n\
             \x20             protocol: {proto}\n",
            name = port.name,
            port = port.port,
            proto = port.protocol,
        ));
    }

    // Env vars from secrets
    if !c.secrets.is_empty() {
        out.push_str("          env:\n");
        for group in &c.secrets {
            let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));
            out.push_str(&format!(
                "            # {} credentials via Secret (figment env cascade overrides file config)\n",
                group.group_name
            ));
            for env in &group.env_vars {
                // See gen_secret_yaml -- hyphenated keys must use index form.
                let key_lookup = safe_template_lookup(
                    &format!(".Values.{}.secretKeys", group.group_name),
                    &env.key_name,
                );
                out.push_str(&format!(
                    "            - name: {env_var}\n\
                     \x20             valueFrom:\n\
                     \x20               secretKeyRef:\n\
                     \x20                 name: {{{{ include \"{app}.{helper}\" . }}}}\n\
                     \x20                 key: {{{{ {key_lookup} }}}}\n",
                    env_var = env.env_var,
                    app = app,
                    helper = helper_name,
                ));
            }
        }
    }

    // Probes
    out.push_str(&format!(
        "          livenessProbe:\n\
         \x20           httpGet:\n\
         \x20             path: {liveness}\n\
         \x20             port: metrics\n\
         \x20           initialDelaySeconds: 10\n\
         \x20           periodSeconds: 10\n\
         \x20           failureThreshold: 3\n\
         \x20         readinessProbe:\n\
         \x20           httpGet:\n\
         \x20             path: {readiness}\n\
         \x20             port: metrics\n\
         \x20           initialDelaySeconds: 5\n\
         \x20           periodSeconds: 5\n\
         \x20           failureThreshold: 2\n\
         \x20         startupProbe:\n\
         \x20           httpGet:\n\
         \x20             path: {liveness}\n\
         \x20             port: metrics\n\
         \x20           failureThreshold: 30\n\
         \x20           periodSeconds: 5\n",
        liveness = c.health.liveness_path,
        readiness = c.health.readiness_path,
    ));

    // Volume mounts
    out.push_str(&format!(
        "          volumeMounts:\n\
         \x20           - name: config\n\
         \x20             mountPath: {config_dir}\n\
         \x20             readOnly: true\n",
        config_dir = c.config_dir(),
    ));

    // Resources
    out.push_str(
        "          {{- with .Values.resources }}\n\
         \x20         resources:\n\
         \x20           {{- toYaml . | nindent 12 }}\n\
         \x20         {{- end }}\n",
    );

    // Volumes
    out.push_str(&format!(
        "      volumes:\n\
         \x20       - name: config\n\
         \x20         configMap:\n\
         \x20           name: {{{{ include \"{app}.fullname\" . }}}}-config\n",
    ));

    // Node selector, affinity, tolerations
    out.push_str(
        "      {{- with .Values.nodeSelector }}\n\
         \x20     nodeSelector:\n\
         \x20       {{- toYaml . | nindent 8 }}\n\
         \x20     {{- end }}\n\
         \x20     {{- with .Values.affinity }}\n\
         \x20     affinity:\n\
         \x20       {{- toYaml . | nindent 8 }}\n\
         \x20     {{- end }}\n\
         \x20     {{- with .Values.tolerations }}\n\
         \x20     tolerations:\n\
         \x20       {{- toYaml . | nindent 8 }}\n\
         \x20     {{- end }}\n",
    );

    out
}

fn gen_service_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    let mut out = format!(
        r#"apiVersion: v1
kind: Service
metadata:
  name: {{{{ include "{app}.fullname" . }}}}
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
spec:
  type: {{{{ .Values.service.type }}}}
  ports:
    - port: {{{{ .Values.service.port }}}}
      targetPort: metrics
      protocol: TCP
      name: metrics
"#,
    );

    // Extra ports
    for port in &c.extra_ports {
        out.push_str(&format!(
            "    - port: {port}\n\
             \x20     targetPort: {port}\n\
             \x20     protocol: {proto}\n\
             \x20     name: {name}\n",
            port = port.port,
            proto = port.protocol,
            name = port.name,
        ));
    }

    out.push_str(&format!(
        "  selector:\n\
         \x20   {{{{- include \"{app}.selectorLabels\" . | nindent 4 }}}}\n",
    ));

    out
}

fn gen_serviceaccount_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    format!(
        r#"{{{{- if .Values.serviceAccount.create -}}}}
apiVersion: v1
kind: ServiceAccount
metadata:
  name: {{{{ include "{app}.serviceAccountName" . }}}}
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
  {{{{- with .Values.serviceAccount.annotations }}}}
  annotations:
    {{{{- toYaml . | nindent 4 }}}}
  {{{{- end }}}}
automountServiceAccountToken: false
{{{{- end }}}}
"#,
    )
}

fn gen_configmap_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;

    let mut out = format!(
        r#"apiVersion: v1
kind: ConfigMap
metadata:
  name: {{{{ include "{app}.fullname" . }}}}-config
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
data:
  {filename}: |
    {{{{- toYaml .Values.config | nindent 4 }}}}
"#,
        app = app,
        filename = c.config_filename(),
    );

    let _ = &mut out; // keep borrow checker happy
    out
}

fn gen_secret_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    let mut out = String::new();
    let mut first = true;

    for group in &c.secrets {
        if !first {
            out.push_str("---\n");
        }
        first = false;

        let helper_name = format!("{}SecretName", to_camel_suffix(&group.group_name));

        out.push_str(&format!(
            "{{{{- if not .Values.{group}.existingSecret }}}}\n\
             apiVersion: v1\n\
             kind: Secret\n\
             metadata:\n\
             \x20 name: {{{{ include \"{app}.{helper}\" . }}}}\n\
             \x20 labels:\n\
             \x20   {{{{- include \"{app}.labels\" . | nindent 4 }}}}\n\
             type: Opaque\n\
             data:\n",
            group = group.group_name,
            app = app,
            helper = helper_name,
        ));

        for env in &group.env_vars {
            // Hyphenated or otherwise non-Go-identifier-safe key names
            // require `(index .Values.x "key")` form instead of
            // `.Values.x.key` -- Go-template parser rejects hyphens etc.
            let key_lookup = safe_template_lookup(
                &format!(".Values.{}.secretKeys", group.group_name),
                &env.key_name,
            );
            let val_lookup =
                safe_template_lookup(&format!(".Values.{}", group.group_name), &env.key_name);
            out.push_str(&format!(
                "  {{{{ {key_lookup} }}}}: {{{{ {val_lookup} | b64enc | quote }}}}\n"
            ));
        }

        out.push_str("{{- end }}\n");
    }

    if c.secrets.is_empty() {
        out.push_str("# No secrets defined in deployment contract\n");
    }

    out
}

fn gen_hpa_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;
    format!(
        r#"{{{{- if and .Values.autoscaling.enabled (not .Values.keda.enabled) }}}}
# Standard HPA fallback -- use when KEDA operator is not installed.
# Mutually exclusive with keda.enabled (KEDA creates its own HPA).
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{{{ include "{app}.fullname" . }}}}
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{{{ include "{app}.fullname" . }}}}
  minReplicas: {{{{ .Values.autoscaling.minReplicas }}}}
  maxReplicas: {{{{ .Values.autoscaling.maxReplicas }}}}
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: {{{{ .Values.autoscaling.targetCPUUtilizationPercentage }}}}
{{{{- end }}}}
"#,
    )
}

fn gen_keda_scaledobject_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;

    // Find kafka secret group for trigger auth reference
    let has_kafka_secret = c.secrets.iter().any(|g| g.group_name == "kafka");

    let auth_ref = if has_kafka_secret {
        format!(
            "      authenticationRef:\n\
             \x20       name: {{{{ include \"{app}.fullname\" . }}}}-kafka-auth\n"
        )
    } else {
        String::new()
    };

    format!(
        r#"{{{{- if .Values.keda.enabled }}}}
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: {{{{ include "{app}.fullname" . }}}}
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
spec:
  scaleTargetRef:
    name: {{{{ include "{app}.fullname" . }}}}
  minReplicaCount: {{{{ .Values.keda.minReplicaCount }}}}
  maxReplicaCount: {{{{ .Values.keda.maxReplicaCount }}}}
  pollingInterval: {{{{ .Values.keda.pollingInterval }}}}
  cooldownPeriod: {{{{ .Values.keda.cooldownPeriod }}}}
  triggers:
    # Kafka consumer group lag (primary scaler)
    - type: kafka
{auth_ref}      metadata:
        bootstrapServers: {{{{ .Values.config.kafka.brokers | quote }}}}
        consumerGroup: {{{{ .Values.keda.kafka.consumerGroup | default .Values.config.kafka.group_id | quote }}}}
        {{{{- /* `default (index X 0)` would eagerly evaluate `index nil 0` and fail
            lint when no topics are set. Use explicit conditional instead. */}}}}
        {{{{- if .Values.keda.kafka.topic }}}}
        topic: {{{{ .Values.keda.kafka.topic | quote }}}}
        {{{{- else if .Values.config.kafka.topics }}}}
        topic: {{{{ (index .Values.config.kafka.topics 0) | quote }}}}
        {{{{- else }}}}
        topic: ""
        {{{{- end }}}}
        lagThreshold: {{{{ .Values.keda.kafka.lagThreshold | quote }}}}
        activationLagThreshold: {{{{ .Values.keda.kafka.activationLagThreshold | quote }}}}
        saslType: scram_sha512
        tls: disable
    {{{{- if .Values.keda.cpu.enabled }}}}
    # CPU utilisation (secondary scaler)
    - type: cpu
      metricType: Utilization
      metadata:
        value: {{{{ .Values.keda.cpu.threshold | quote }}}}
    {{{{- end }}}}
{{{{- end }}}}
"#,
    )
}

fn gen_keda_triggerauth_yaml(c: &DeploymentContract) -> String {
    let app = &c.app_name;

    // Find the kafka secret group
    let kafka_group = c.secrets.iter().find(|g| g.group_name == "kafka");

    if kafka_group.is_none() {
        return "# No kafka secret group -- KEDA TriggerAuthentication not generated\n".to_string();
    }

    let helper_name = format!("{}SecretName", to_camel_suffix("kafka"));

    format!(
        r#"{{{{- if .Values.keda.enabled }}}}
apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: {{{{ include "{app}.fullname" . }}}}-kafka-auth
  labels:
    {{{{- include "{app}.labels" . | nindent 4 }}}}
spec:
  secretTargetRef:
    - parameter: sasl
      name: {{{{ include "{app}.{helper_name}" . }}}}
      key: {{{{ .Values.kafka.secretKeys.username }}}}
    - parameter: password
      name: {{{{ include "{app}.{helper_name}" . }}}}
      key: {{{{ .Values.kafka.secretKeys.password }}}}
{{{{- end }}}}
"#,
    )
}

fn gen_notes_txt(c: &DeploymentContract) -> String {
    let app = &c.app_name;

    format!(
        r#"{app} has been deployed.

1. Get the metrics/health endpoint:
   kubectl port-forward svc/{{{{ include "{app}.fullname" . }}}} {{{{ .Values.service.port }}}}:{{{{ .Values.service.port }}}}
   curl http://localhost:{{{{ .Values.service.port }}}}{liveness}
   curl http://localhost:{{{{ .Values.service.port }}}}{metrics}

{{{{- if .Values.keda.enabled }}}}

2. Check KEDA autoscaling status:
   kubectl get scaledobject {{{{ include "{app}.fullname" . }}}}
   kubectl get hpa
{{{{- end }}}}

3. View logs:
   kubectl logs -l app.kubernetes.io/name={{{{ include "{app}.name" . }}}} -f
"#,
        app = app,
        liveness = c.health.liveness_path,
        metrics = c.health.metrics_path,
    )
}
