#!/usr/bin/env bash

# Updates come from three places that fail independently, and the page must not
# claim to know more than it checked.
#
# The rules:
#
#   1. Every status a health check can return must be one the doctor counts.
#      "degraded" is not in its vocabulary; a check returning it was counted as
#      nothing at all while the summary still said healthy. That is the silent
#      no-op this whole codebase keeps relearning.
#   2. A count nobody verified is not a count. "Up to date" may only be said
#      after a check actually ran.
#   3. Applying packages takes a restore point first, and a failure to take one
#      must not block the update.
#   4. Checking is separate from opening. A nine-second scan on every page open
#      would make Settings feel broken.
#
# Read-only: it reads update state and never installs anything.

set -uo pipefail

repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
helper="$repo_dir/config/dot/quickshell/scripts/panama-updates"
service="$repo_dir/config/dot/quickshell/services/Updates.qml"
page="$repo_dir/config/dot/quickshell/modules/settings/UpdatesPage.qml"
doctor="$repo_dir/config/dot/quickshell/scripts/panama-doctor"

fail() {
    printf 'updates contract: %s\n' "$1" >&2
    exit 1
}

for path in "$helper" "$service" "$page" "$doctor"; do
    [[ -r "$path" ]] || fail "missing $path"
done
[[ -x "$helper" ]] || fail 'panama-updates is not executable'

# ── 1. Every health status is one the doctor counts ─────────────────────────
statuses="$(sed -n 's/^Status = Literal\[\(.*\)\]$/\1/p' "$doctor" | tr -d '" ' | tr ',' '\n' | grep -v '^$')"
[[ -n "$statuses" ]] || fail 'could not read the doctor status vocabulary'
while read -r used; do
    [[ -n "$used" ]] || continue
    grep -qx "$used" <<<"$statuses" \
        || fail "a check reports status \"$used\", which the doctor does not count -- it would be invisible in the summary"
done < <(grep -oE 'Check\("[a-z.]+", "[a-z-]+", "[^"]+", "[a-z]+"' "$doctor" \
         | sed -E 's/.*, "([a-z]+)"$/\1/' | sort -u)

# ── 2. No confident answer without a check ──────────────────────────────────
grep -q 'everChecked' "$service" \
    || fail 'the service cannot tell "no updates" from "never looked"'
grep -q 'Not checked yet' "$service" \
    || fail 'a machine that has never checked is reported as up to date'

# ── 3. Packages get a restore point, best effort ────────────────────────────
apply_body="$(sed -n '/^def apply/,/^def /p' "$helper")"
grep -q 'take_restore_point' <<<"$apply_body" \
    || fail 'installing packages does not take a snapshot first'
restore_body="$(sed -n '/^def take_restore_point/,/^def /p' "$helper")"
grep -q 'return ""' <<<"$restore_body" \
    || fail 'a failed snapshot has no non-fatal path, so it would block the update'
grep -qE 'raise BoundaryError' <<<"$restore_body" \
    && fail 'a failed snapshot aborts the update, which is worse than an update without a restore point'

# Firmware and applications must NOT take a system snapshot: neither changes
# the system tree, and a restore point that restores nothing is noise.
grep -qE 'take_restore_point.*firmware|firmware.*take_restore_point' <<<"$apply_body" \
    && fail 'firmware updates take a system snapshot, which would restore nothing'

# ── 4. Opening is not checking ──────────────────────────────────────────────
grep -q 'def snapshot' "$helper" || fail 'there is no cheap read'
snapshot_body="$(sed -n '/^def snapshot/,/^def /p' "$helper")"
grep -qE 'dnf_updates\(\)|flatpak_updates\(\)|firmware_updates\(\)' <<<"$snapshot_body" \
    && fail 'the cheap read runs the expensive scan, so every page open would wait on the network'
grep -q 'read_cache()' <<<"$snapshot_body" \
    || fail 'the cheap read does not use the cached result'

command -v jq >/dev/null 2>&1 || { printf 'updates contract: SKIP (no jq)\n'; exit 0; }

# ── The snapshot is fast and complete ───────────────────────────────────────
started="$(date +%s)"
state="$("$helper" snapshot 2>/dev/null)" || fail 'snapshot failed'
elapsed=$(( $(date +%s) - started ))
(( elapsed <= 5 )) || fail "the cheap read took ${elapsed}s; it is supposed to be instant"

jq -e '(.dnf | type == "object") and (.flatpak | type == "object") and (.firmware | type == "object")' \
    <<<"$state" >/dev/null || fail 'the snapshot is missing one of the three sources'
jq -e '.kernel | has("running") and has("newestInstalled") and has("rebootNeeded")' <<<"$state" >/dev/null \
    || fail 'the kernel state is incomplete'
jq -e '.automatic | has("flatpakEnabled")' <<<"$state" >/dev/null \
    || fail 'automatic update state is missing'

# The reboot signal must be derived, not guessed.
jq -e '.kernel.rebootNeeded == (.kernel.running != .kernel.newestInstalled)' <<<"$state" >/dev/null \
    || fail 'the reboot signal does not follow from the running and installed kernels'

# ── Refusals ────────────────────────────────────────────────────────────────
[[ -n "$("$helper" apply nonsense 2>/dev/null | jq -r '.error // ""')" ]] \
    || fail 'an unknown update source was accepted'
[[ -n "$("$helper" bogus 2>/dev/null | jq -r '.error // ""')" ]] \
    || fail 'an unknown command was accepted'


# ── Update history ──────────────────────────────────────────────────────────
#
# Worth showing because automatic updates leave no other trace: something that
# installed itself overnight is invisible until somebody looks here.
#
# Both sources are asked in their own machine-readable form. flatpak's is the
# awkward one -- it prints a JSON array whose timestamps are "Aug 20 08:07:46",
# with no year -- so the year is inferred, and a date that would land in the
# future is read as last year's. dnf's start_time is epoch UTC; its own table
# prints that value as though it were local, which it is not.

history="$("$helper" history)" || fail 'history failed'
printf '%s' "$history" | python3 -c "
import json, sys, time
payload = json.load(sys.stdin)
entries = payload['entries']
if not entries:
    raise SystemExit(0)   # a machine with no transactions is legitimate

now = int(time.time())
previous = None
for entry in entries:
    if entry['source'] not in ('dnf', 'flatpak'):
        raise SystemExit(f\"unknown source {entry['source']}\")
    at = int(entry['at'])
    if at and at > now + 86400:
        raise SystemExit(f\"{entry['summary']} is dated in the future, so the year was read wrong\")
    if at and previous is not None and at > previous:
        raise SystemExit('entries are not newest first')
    if at:
        previous = at
    if not str(entry['summary']).strip():
        raise SystemExit('an entry has nothing to say it did')
if not any(e['source'] == 'flatpak' for e in entries) and not any(e['source'] == 'dnf' for e in entries):
    raise SystemExit('neither source produced anything, so nothing was parsed')
" || fail 'the update history is not usable'

printf 'updates contract: PASS (%s dnf, %s flatpak, %s firmware; reboot needed: %s)\n' \
    "$(jq -r '.dnf.count // 0' <<<"$state")" \
    "$(jq -r '.flatpak.count // 0' <<<"$state")" \
    "$(jq -r '.firmware.count // 0' <<<"$state")" \
    "$(jq -r '.kernel.rebootNeeded' <<<"$state")"
