// Proof that the 30-second health heartbeat works end to end, over MQTT.
//
// Publishes a heartbeat to the live broker as a throwaway terminal, then polls
// the public API until it shows online — and keeps polling past the TTL so the
// automatic expiry is visible too. Nothing is written to Postgres; presence
// lives in Redis under a TTL and cleans itself up.
//
// REQUIRES pos_terminal CREDENTIALS. The MQTT_USER in .env is pos_ingest, which
// the broker ACL deliberately denies publish on the health topic — it may only
// write acks and the catalogue. Running this with the ingest account connects
// fine and then silently drops every publish, because Mosquitto answers an
// ACL-denied QoS 1 publish with a PUBACK and discards it. That looks exactly
// like a broken consumer and cost an hour of misdiagnosis; set MQTT_USER and
// MQTT_PASSWORD to the terminal account before believing a negative result.
//
// The HTTP path needs none of this — see POST /pos/health, which is what the
// fix on v1.3.96 added and how the endpoint was actually verified.
//
//	go run ./scratch/healthproof
package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	mqtt "github.com/eclipse/paho.mqtt.golang"
	"github.com/joho/godotenv"
)

const (
	locationID = "1135"
	terminalID = "TPROOF" // throwaway; disappears on its own when the TTL lapses
	apiBase    = "https://fiesta.nearle.app/live/api/v1/pos"
)

func main() {
	_ = godotenv.Load()

	brokerURL := os.Getenv("MQTT_URL")
	if brokerURL == "" {
		log.Fatal("MQTT_URL not set")
	}

	opts := mqtt.NewClientOptions().
		AddBroker(brokerURL).
		SetClientID("healthproof-" + terminalID).
		SetUsername(os.Getenv("MQTT_USER")).
		SetPassword(os.Getenv("MQTT_PASSWORD")).
		SetConnectTimeout(10 * time.Second)

	client := mqtt.NewClient(opts)
	if t := client.Connect(); t.Wait() && t.Error() != nil {
		log.Fatal("connect: ", t.Error())
	}
	defer client.Disconnect(250)
	fmt.Printf("connected to %s as %s\n\n", brokerURL, terminalID)

	fmt.Println("BEFORE — has this terminal ever been seen?")
	show()

	// Exactly what the till sends every 30 seconds.
	health, _ := json.Marshal(map[string]any{
		"schema": 1, "status": "online",
		"terminal_id": terminalID, "location_id": locationID,
		"store_name": "Ragul stores Selvapuram", "app_version": "1.1.0",
		"pending_bills": 0, "pending_registrations": 0,
		"today_bills": 17, "today_amount": 2510.0,
		"printer_reachable": true,
		"reported_at":       time.Now().UTC().Format(time.RFC3339),
	})

	topic := fmt.Sprintf("nearle/pos/%s/%s/health", locationID, terminalID)
	if t := client.Publish(topic, 1, false, health); t.Wait() && t.Error() != nil {
		log.Fatal("publish: ", t.Error())
	}
	fmt.Printf("\npublished one heartbeat to %s\n", topic)

	time.Sleep(2 * time.Second)
	fmt.Println("\nAFTER one heartbeat:")
	show()

	// The TTL is 90s and a real till refreshes every 30s, so it never lapses
	// while the till is alive. Stopping here is what a till being switched off
	// looks like.
	fmt.Println("\nnow going quiet, as a till that was switched off would.")
	fmt.Println("presence TTL is 90s, so it should drop off on its own:")

	for _, wait := range []int{30, 30, 35} {
		time.Sleep(time.Duration(wait) * time.Second)
		fmt.Printf("\n+%ds since the last heartbeat:\n", wait)
		show()
	}
}

func show() {
	resp, err := http.Get(fmt.Sprintf("%s/health/location?location_id=%s", apiBase, locationID))
	if err != nil {
		fmt.Println("   API unreachable:", err)
		return
	}
	defer resp.Body.Close()
	body, _ := io.ReadAll(resp.Body)

	var out struct {
		Details struct {
			Online    int `json:"online"`
			Total     int `json:"total"`
			Terminals []struct {
				Terminalid  string  `json:"terminal_id"`
				Status      string  `json:"status"`
				Reason      string  `json:"reason"`
				Todaybills  int     `json:"today_bills"`
				Todayamount float64 `json:"today_amount"`
				Reportedat  string  `json:"reported_at"`
			} `json:"terminals"`
		} `json:"details"`
	}
	if err := json.Unmarshal(body, &out); err != nil {
		fmt.Println("   unparseable:", string(body)[:200])
		return
	}

	fmt.Printf("   online %d of %d\n", out.Details.Online, out.Details.Total)
	for _, t := range out.Details.Terminals {
		mark := " "
		if t.Terminalid == terminalID {
			mark = ">"
		}
		extra := t.Reason
		if t.Status == "online" {
			extra = fmt.Sprintf("today %d bills / Rs %.0f, reported %s",
				t.Todaybills, t.Todayamount, t.Reportedat)
		}
		fmt.Printf("   %s %-8s %-8s %s\n", mark, t.Terminalid, t.Status, extra)
	}
}
