// Package observability is extended in Phase 14 with a Prometheus metrics
// endpoint and a heartbeat writer. The Phase 0 EventRecorder + HealthChecker
// remain unchanged.
package observability

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"sync"
	"sync/atomic"
	"time"
)

// Metrics holds the per-process counters/gauges/histograms surfaced at
// /metrics. Phase 14 ships a minimal native Prometheus exposition rather
// than pulling in the official client_golang library; the format is
// stable and the operator can scrape it directly. If the surface grows
// beyond ~20 metrics, swap to client_golang.
type Metrics struct {
	mu sync.RWMutex

	httpRequests     map[string]uint64 // "method:path:status" → count
	httpDurationsMS  map[string][]uint64
	scrapeOutcomes   map[string]uint64 // "tracker:classification" → count
	integrationHealth map[string]int32
	eventBusPublished map[string]uint64
	eventBusDropped   map[string]uint64

	buildVersion string
	startedAt    time.Time
}

// NewMetrics constructs an empty registry.
func NewMetrics(version string) *Metrics {
	return &Metrics{
		httpRequests:      map[string]uint64{},
		httpDurationsMS:   map[string][]uint64{},
		scrapeOutcomes:    map[string]uint64{},
		integrationHealth: map[string]int32{},
		eventBusPublished: map[string]uint64{},
		eventBusDropped:   map[string]uint64{},
		buildVersion:      version,
		startedAt:         time.Now(),
	}
}

// RecordHTTPRequest is called by the access-log middleware after each request.
func (m *Metrics) RecordHTTPRequest(method, path string, status int, dur time.Duration) {
	key := fmt.Sprintf("%s:%s:%d", method, path, status)
	m.mu.Lock()
	m.httpRequests[key]++
	durKey := fmt.Sprintf("%s:%s", method, path)
	m.httpDurationsMS[durKey] = append(m.httpDurationsMS[durKey], uint64(dur.Milliseconds()))
	if len(m.httpDurationsMS[durKey]) > 1000 {
		m.httpDurationsMS[durKey] = m.httpDurationsMS[durKey][500:]
	}
	m.mu.Unlock()
}

// RecordScrape records a scrape outcome.
func (m *Metrics) RecordScrape(tracker, classification string) {
	m.mu.Lock()
	m.scrapeOutcomes[tracker+":"+classification]++
	m.mu.Unlock()
}

// SetIntegrationHealth marks an integration as 1 (healthy) or 0 (unhealthy).
func (m *Metrics) SetIntegrationHealth(integration string, healthy bool) {
	v := int32(0)
	if healthy {
		v = 1
	}
	m.mu.Lock()
	m.integrationHealth[integration] = v
	m.mu.Unlock()
}

// RecordEventBusPublish + RecordEventBusDrop are called from the bus.
func (m *Metrics) RecordEventBusPublish(topic string) {
	m.mu.Lock()
	m.eventBusPublished[topic]++
	m.mu.Unlock()
}
func (m *Metrics) RecordEventBusDrop(topic string) {
	m.mu.Lock()
	m.eventBusDropped[topic]++
	m.mu.Unlock()
}

// Handler renders the Prometheus exposition format. Phase 14 keeps this
// minimal: counter and gauge types only, no histograms (we summarize
// durations as p50/p95 gauges per route).
func (m *Metrics) Handler() http.HandlerFunc {
	return func(w http.ResponseWriter, _ *http.Request) {
		m.mu.RLock()
		defer m.mu.RUnlock()
		w.Header().Set("Content-Type", "text/plain; version=0.0.4")
		fmt.Fprintf(w, "# HELP command_center_build_info Static build info.\n")
		fmt.Fprintf(w, "# TYPE command_center_build_info gauge\n")
		fmt.Fprintf(w, "command_center_build_info{version=%q} 1\n", m.buildVersion)
		fmt.Fprintf(w, "# HELP command_center_uptime_seconds Process uptime.\n")
		fmt.Fprintf(w, "# TYPE command_center_uptime_seconds gauge\n")
		fmt.Fprintf(w, "command_center_uptime_seconds %d\n", int64(time.Since(m.startedAt).Seconds()))

		fmt.Fprintf(w, "# HELP command_center_request_total HTTP request count.\n")
		fmt.Fprintf(w, "# TYPE command_center_request_total counter\n")
		for k, v := range m.httpRequests {
			fmt.Fprintf(w, "command_center_request_total{key=%q} %d\n", k, v)
		}

		fmt.Fprintf(w, "# HELP command_center_scrape_total Scrape outcome count.\n")
		fmt.Fprintf(w, "# TYPE command_center_scrape_total counter\n")
		for k, v := range m.scrapeOutcomes {
			fmt.Fprintf(w, "command_center_scrape_total{key=%q} %d\n", k, v)
		}

		fmt.Fprintf(w, "# HELP command_center_integration_health Integration health gauge.\n")
		fmt.Fprintf(w, "# TYPE command_center_integration_health gauge\n")
		for k, v := range m.integrationHealth {
			fmt.Fprintf(w, "command_center_integration_health{integration=%q} %d\n", k, v)
		}

		fmt.Fprintf(w, "# HELP command_center_event_bus_published_total Eventbus publish count.\n")
		fmt.Fprintf(w, "# TYPE command_center_event_bus_published_total counter\n")
		for k, v := range m.eventBusPublished {
			fmt.Fprintf(w, "command_center_event_bus_published_total{topic=%q} %d\n", k, v)
		}
		fmt.Fprintf(w, "# HELP command_center_event_bus_subscriber_dropped_total Eventbus drop count.\n")
		fmt.Fprintf(w, "# TYPE command_center_event_bus_subscriber_dropped_total counter\n")
		for k, v := range m.eventBusDropped {
			fmt.Fprintf(w, "command_center_event_bus_subscriber_dropped_total{topic=%q} %d\n", k, v)
		}
	}
}

// Heartbeat writes a timestamp file every interval. The operator's
// independent cron watcher reads the mtime to determine liveness.
type Heartbeat struct {
	path     string
	interval time.Duration
	stop     atomic.Bool
}

// NewHeartbeat constructs a Heartbeat. interval defaults to 60s when zero.
func NewHeartbeat(path string, interval time.Duration) *Heartbeat {
	if interval <= 0 {
		interval = 60 * time.Second
	}
	return &Heartbeat{path: path, interval: interval}
}

// Start writes the heartbeat file periodically. Run in a goroutine.
func (h *Heartbeat) Start(ctx context.Context) {
	if h.path == "" {
		return
	}
	t := time.NewTicker(h.interval)
	defer t.Stop()
	_ = h.write()
	for {
		select {
		case <-ctx.Done():
			return
		case <-t.C:
			if h.stop.Load() {
				return
			}
			_ = h.write()
		}
	}
}

// Stop signals the goroutine to exit.
func (h *Heartbeat) Stop() { h.stop.Store(true) }

func (h *Heartbeat) write() error {
	now := time.Now().UTC().Format(time.RFC3339)
	payload := map[string]string{"timestamp": now}
	b, _ := json.Marshal(payload)
	return os.WriteFile(h.path, b, 0o644)
}
