package main

import (
	"bytes"
	"crypto/rand"
	"encoding/json"
	"fmt"
	"net/http"
	"runtime"
	"strings"
	"time"

	"hamlog/internal/applog"
)

// Anonymous usage telemetry - a once-a-day "app_opened" heartbeat to PostHog so
// the OpsLog author can see how many people actively use it. Privacy by design:
// only a random install ID + app version + OS are sent (no callsign, no QSO
// data, no IP beyond what any HTTP request reveals). Users can disable it in
// Preferences -> General. See [[user-analytics-posthog]] notes in MEMORY.

const (
	// appVersion is stamped on every heartbeat (and could feed the About box).
	appVersion = "0.27.17"

	// posthogHost is the PostHog ingestion endpoint. EU cloud by default; change
	// to https://us.i.posthog.com for a US project.
	posthogHost = "https://eu.i.posthog.com"

	// posthogAPIKey is the PostHog PROJECT API key ("phc_..."). It's a public,
	// write-only ingestion key (safe to ship in the binary, like the ClubLog app
	// key). Until it's filled in, telemetry is a no-op.
	posthogAPIKey = "phc_vumvN7XTERNhmRzMZHNgY5DncZfFibTbomiE9epZvUJ4"

	keyTelemetryEnabled   = "telemetry.enabled"    // "0" disables; default on
	keyTelemetryInstallID = "telemetry.install_id" // random, stable per install
	keyTelemetryLastSent  = "telemetry.last_sent"  // YYYY-MM-DD of last heartbeat
)

// GetTelemetryEnabled reports whether anonymous usage stats are on (default on).
func (a *App) GetTelemetryEnabled() bool {
	if a.settings == nil {
		return true
	}
	v, _ := a.settings.GetGlobal(a.ctx, keyTelemetryEnabled)
	return strings.TrimSpace(v) != "0"
}

// SetTelemetryEnabled turns anonymous usage stats on or off.
func (a *App) SetTelemetryEnabled(on bool) error {
	if a.settings == nil {
		return fmt.Errorf("db not initialized")
	}
	val := "1"
	if !on {
		val = "0"
	}
	return a.settings.SetGlobal(a.ctx, keyTelemetryEnabled, val)
}

// telemetryInstallID returns this install's stable anonymous ID, generating and
// persisting one on first use.
func (a *App) telemetryInstallID() string {
	id, _ := a.settings.GetGlobal(a.ctx, keyTelemetryInstallID)
	if id = strings.TrimSpace(id); id != "" {
		return id
	}
	id = randomUUID()
	_ = a.settings.SetGlobal(a.ctx, keyTelemetryInstallID, id)
	return id
}

// sendTelemetryHeartbeat sends at most one "app_opened" event per calendar day
// (UTC). Best effort: any failure is logged and ignored. Runs in a goroutine at
// startup.
func (a *App) sendTelemetryHeartbeat() {
	if a.settings == nil || !a.GetTelemetryEnabled() {
		return
	}
	if strings.Contains(posthogAPIKey, "REPLACE") || strings.TrimSpace(posthogAPIKey) == "" {
		return // not configured yet
	}
	today := time.Now().UTC().Format("2006-01-02")
	if last, _ := a.settings.GetGlobal(a.ctx, keyTelemetryLastSent); strings.TrimSpace(last) == today {
		return // already counted today
	}

	// distinct_id identifies the USER. Prefer the station callsign — it's stable
	// across every machine/reinstall the same operator runs, so one op counts as
	// one user (a callsign is public in amateur radio). The random per-install ID
	// is only a fallback; without it the same op on a laptop + desktop + Maestro
	// showed up as three "users". install_id rides along as a property so multiple
	// machines under one callsign stay visible.
	//
	// On a FRESH INSTALL the callsign isn't configured yet at launch, so sending
	// straight away would record the machine's random UUID (and the once-a-day lock
	// would then keep it that way even after the op enters their call). Instead wait
	// a while for the callsign to appear (Settings → Station Information) and only
	// fall back to the UUID if none shows up within the grace window — so a genuine
	// no-callsign install is still counted, but the normal case gets the callsign.
	call := a.activeCallsign()
	for i := 0; call == "" && i < 60; i++ { // up to ~10 min (60 × 10 s)
		time.Sleep(10 * time.Second)
		if a.settings == nil || !a.GetTelemetryEnabled() {
			return // disabled meanwhile
		}
		if last, _ := a.settings.GetGlobal(a.ctx, keyTelemetryLastSent); strings.TrimSpace(last) == today {
			return // sent by another path in the meantime
		}
		call = a.activeCallsign()
	}
	installID := a.telemetryInstallID()
	distinctID := installID
	if call != "" {
		distinctID = call
	}
	payload := map[string]any{
		"api_key":     posthogAPIKey,
		"event":       "app_opened",
		"distinct_id": distinctID,
		"properties": map[string]any{
			"version":    appVersion,
			"os":         runtime.GOOS,
			"arch":       runtime.GOARCH,
			"install_id": installID,
			"$lib":       "opslog",
		},
		"timestamp": time.Now().UTC().Format(time.RFC3339),
	}
	body, err := json.Marshal(payload)
	if err != nil {
		return
	}
	client := &http.Client{Timeout: 15 * time.Second}
	resp, err := client.Post(posthogHost+"/capture/", "application/json", bytes.NewReader(body))
	if err != nil {
		applog.Printf("telemetry: heartbeat failed: %v", err)
		return
	}
	resp.Body.Close()
	if resp.StatusCode >= 200 && resp.StatusCode < 300 {
		_ = a.settings.SetGlobal(a.ctx, keyTelemetryLastSent, today)
		applog.Printf("telemetry: heartbeat ok (%s)", today)
	} else {
		applog.Printf("telemetry: heartbeat HTTP %d", resp.StatusCode)
	}
}

// randomUUID returns a random RFC-4122 v4 UUID string.
func randomUUID() string {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil {
		// Extremely unlikely; fall back to a time-based id so we still get one.
		return fmt.Sprintf("ts-%d", time.Now().UnixNano())
	}
	b[6] = (b[6] & 0x0f) | 0x40 // version 4
	b[8] = (b[8] & 0x3f) | 0x80 // variant 10
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
}
