# just is a command runner, Justfile is very similar to Makefile, but simpler.
############################################################################
#
#  Common recipes
#
############################################################################

# List all recipes.
_default:
    @printf '\033[1;36mnixcfg recipes\033[0m\n\n'
    @printf '\033[1;33mUsage:\033[0m just <recipe> [args...]\n\n'
    @just --list --list-heading $'Available recipes:\n\n'

# Regenerate editor config (.zed/settings.json).
[group('flake')]
gen:
    nix run .#gen-files

# Update flake inputs.
[group('flake')]
update *inputs:
    nix flake update {{ inputs }} --commit-lock-file

# Update all nixpkgs inputs.
[group('flake')]
update-nixpkgs: (update "nixpkgs")

# Update caddy-tailscale plugin to latest commit (also refreshes hash if stale).
[group('flake')]
update-caddy-tailscale:
    bun scripts/update-caddy-tailscale.ts

############################################################################
#
#  Servers
#
############################################################################

# Build a NixOS host locally without deploying.
[group('servers')]
build host:
    nix build .#nixosConfigurations.{{host}}.config.system.build.toplevel

# Deploy hosts with nynx.
[group('servers')]
deploy jobs='':
    nynx --operation switch {{ if jobs == "" { "" } else { "--jobs " + jobs } }}

# Reboot all servers.
[group('servers')]
reboot:
    ansible-playbook -i ansible/inventory.ini ansible/playbooks/reboot.yml

# Ping all servers.
[group('servers')]
ping:
    ansible-playbook -i ansible/inventory.ini ansible/playbooks/ping.yml

# Queue offline deployment & reboot.
[group('servers')]
deploy-offline:
    nynx --operation boot
    just reboot

############################################################################
#
#  Secrets (sops + age)
#
############################################################################

# Derive this machine's age private key from its ssh ed25519 key, install
# at ~/.config/sops/age/keys.txt. Run once per machine. The corresponding
# public key (age1...) must already be a recipient in .sops.yaml.
[group('secrets')]
sops-bootstrap:
    #!/usr/bin/env bash
    set -euo pipefail
    if [[ -f ~/.config/sops/age/keys.txt ]]; then
        echo "~/.config/sops/age/keys.txt already exists; skipping."
        exit 0
    fi
    mkdir -p ~/.config/sops/age
    ssh-to-age -private-key -i ~/.ssh/id_ed25519 > ~/.config/sops/age/keys.txt
    chmod 600 ~/.config/sops/age/keys.txt
    echo "wrote ~/.config/sops/age/keys.txt"
    echo "this machine's age recipient (must be in .sops.yaml):"
    ssh-to-age -i ~/.ssh/id_ed25519.pub

# Regenerate .sops.yaml from keys/*.pub, then re-encrypt every
# secrets/*.yaml. Run after adding or removing a .pub file.
[group('secrets')]
sops-rekey:
    #!/usr/bin/env bash
    set -euo pipefail
    shopt -s nullglob
    user_pubs=(keys/aly_*.pub)
    host_pubs=(keys/root_*.pub)
    flux_pubs=(keys/flux_*.pub)
    {
        cat <<'EOF'
    # Auto-generated by `just sops-rekey` from keys/*.pub.
    # User and host keys decrypt legacy secrets/*.yaml. Flux keys decrypt
    # only k8s/flux/secrets/*.sops.yaml.
    keys:
      # === user keys (aly@<host>) ===
    EOF
        for f in "${user_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#aly_}
            age=$(ssh-to-age -i "$f")
            printf '  - &user_aly_%s %s\n' "$host" "$age"
        done
        echo "  # === host keys (root@<host>) ==="
        for f in "${host_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#root_}
            age=$(ssh-to-age -i "$f")
            printf '  - &host_%s %s\n' "$host" "$age"
        done
        if ((${#flux_pubs[@]} > 0)); then
            echo "  # === flux keys ==="
            for f in "${flux_pubs[@]}"; do
                name=$(basename "$f" .pub); name=${name#flux_}
                age=$(<"$f")
                printf '  - &flux_%s %s\n' "$name" "$age"
            done
        fi
        cat <<'EOF'

    creation_rules:
      - path_regex: ^k8s/flux/secrets/.*\.sops\.ya?ml$
        key_groups:
          - age:
    EOF
        for f in "${user_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#aly_}
            printf '          - *user_aly_%s\n' "$host"
        done
        for f in "${host_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#root_}
            printf '          - *host_%s\n' "$host"
        done
        for f in "${flux_pubs[@]}"; do
            name=$(basename "$f" .pub); name=${name#flux_}
            printf '          - *flux_%s\n' "$name"
        done
        cat <<'EOF'

      - path_regex: ^secrets/.*\.ya?ml$
        key_groups:
          - age:
    EOF
        for f in "${user_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#aly_}
            printf '          - *user_aly_%s\n' "$host"
        done
        for f in "${host_pubs[@]}"; do
            host=$(basename "$f" .pub); host=${host#root_}
            printf '          - *host_%s\n' "$host"
        done
    } > .sops.yaml
    echo "regenerated .sops.yaml"
    for f in secrets/*.yaml; do
        echo "rekeying $f"
        sops updatekeys -y "$f"
    done

# Edit a sops-encrypted secrets file. Usage: just sops-edit vaultwarden.yaml
[group('secrets')]
sops-edit FILE:
    sops secrets/{{FILE}}

############################################################################
#
# Kubes (k3s + flux)
#
############################################################################

# Run all consistency + flake checks.
[group('kubes')]
check:
    bun scripts/check.ts
    for dir in k8s/flux/system k8s/flux/sources k8s/flux/secrets k8s/flux/infra-crds k8s/flux/infra-core k8s/flux/platform k8s/flux/apps k8s/flux/external-routes; do kubectl kustomize "$dir" >/dev/null; done
    nix flake check --impure

# Render every HelmRelease the way Flux does (resolving valuesFrom), like CI.
# flux-local isn't packaged in nixpkgs; run it via uv (uses the devShell's
# kustomize/helm/flux binaries).
[group('kubes')]
flux-test:
    uvx flux-local@8.3.0 test --enable-helm --path k8s/flux

# Bump a digest-pinned chart image to its current upstream digest.
# Usage: just bump <chart> | just bump --all | just bump --check
[group('kubes')]
bump TARGET:
    bun scripts/bump-image.ts {{ TARGET }}

# Real-release helper for local in-tree charts only. Flux HelmRelease objects
# live in flux-system; Helm installs into the target namespace.
# Usage: just k8s {apply|diff|suspend|resume|reconcile} <release> [target-namespace] [helmrelease-namespace]
[group('kubes')]
k8s action release namespace='default' hr_namespace='flux-system':
    #!/usr/bin/env bash
    set -euo pipefail
    chart="k8s/charts/{{release}}"
    flux_cmd() {
      if command -v flux >/dev/null 2>&1; then
        flux "$@"
      else
        nix develop -c flux "$@"
      fi
    }

    # Render the cute-haus-global values to a temp file — the same ConfigMap
    # Flux feeds every chart via valuesFrom, so local apply/diff match runtime.
    global_values() {
      local out; out="$(mktemp)"
      kubectl kustomize k8s/flux/sources \
        | yq e 'select(.kind == "ConfigMap" and .metadata.name == "cute-haus-global") | .data["values.yaml"]' - \
        > "$out"
      echo "$out"
    }

    case "{{action}}" in
      apply)
        if [[ ! -f "$chart/Chart.yaml" ]]; then
          echo "just k8s apply supports local charts only: $chart/Chart.yaml not found" >&2
          exit 2
        fi
        values="$(global_values)"; trap 'rm -f "$values"' EXIT
        flux_cmd suspend helmrelease "{{release}}" -n "{{hr_namespace}}" || true
        helm upgrade --install "{{release}}" "$chart" \
          -n "{{namespace}}" -f "$values"
        ;;
      diff)
        if [[ ! -f "$chart/Chart.yaml" ]]; then
          echo "just k8s diff supports local charts only: $chart/Chart.yaml not found" >&2
          exit 2
        fi
        values="$(global_values)"; trap 'rm -f "$values"' EXIT
        helm diff upgrade --allow-unreleased "{{release}}" "$chart" \
          -n "{{namespace}}" -f "$values"
        ;;
      suspend)
        flux_cmd suspend helmrelease "{{release}}" -n "{{hr_namespace}}"
        ;;
      resume)
        flux_cmd resume helmrelease "{{release}}" -n "{{hr_namespace}}"
        ;;
      reconcile)
        flux_cmd reconcile helmrelease "{{release}}" -n "{{hr_namespace}}" --with-source
        ;;
      *)
        echo "usage: just k8s {apply|diff|suspend|resume|reconcile} <release> [target-namespace] [helmrelease-namespace]" >&2
        exit 2
        ;;
    esac

# Bump tranquil-pds to its current atcr.io digest. Separate from `just bump`
# because atcr.io is private — `skopeo --no-creds` can't reach it.
[group('kubes')]
bump-tranquil:
    #!/usr/bin/env bash
    set -euo pipefail
    TEMPLATE=k8s/charts/tranquil-pds/templates/deployment.yaml
    FULL=$(grep 'image:.*@sha256:' "$TEMPLATE" | awk '{print $2}')
    REPO="${FULL%%:*}"
    TAG_WITH_DIGEST="${FULL#*:}"
    FLOAT_TAG="${TAG_WITH_DIGEST%%@*}"
    CURRENT_DIGEST="${TAG_WITH_DIGEST#*@}"
    USERNAME=$(sops -d --extract '["username"]' secrets/atcr.yaml)
    PASSWORD=$(sops -d --extract '["password"]' secrets/atcr.yaml)
    UPSTREAM=$(skopeo inspect --creds "$USERNAME:$PASSWORD" \
        "docker://${REPO}:${FLOAT_TAG}" --format '{{"{{"}}.Digest{{"}}"}}')
    if [[ "$UPSTREAM" == "$CURRENT_DIGEST" ]]; then
        echo "tranquil-pds: ✓ up to date"
        exit 0
    fi
    echo "tranquil-pds: ${CURRENT_DIGEST:7:12} → ${UPSTREAM:7:12}"
    sed -i "s|@${CURRENT_DIGEST}|@${UPSTREAM}|" "$TEMPLATE"
    git add "$TEMPLATE"
    git commit -m "k8s/tranquil-pds: ${FLOAT_TAG}@${CURRENT_DIGEST:7:12} → ${FLOAT_TAG}@${UPSTREAM:7:12}"
