// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause

//go:build !plan9

// Package health contains shared types and underlying methods for serving
// a `/healthz` endpoint for containerboot and k8s-proxy.
package health

import (
	"context"
	"fmt"
	"net/http"
	"sync"

	"tailscale.com/client/local"
	"tailscale.com/ipn"
	"tailscale.com/kube/kubetypes"
	"tailscale.com/types/logger"
)

// Healthz is a simple health check server, if enabled it returns 200 OK if
// this tailscale node currently has at least one tailnet IP address else
// returns 503.
type Healthz struct {
	sync.Mutex
	hasAddrs bool
	podIPv4  string
	logger   logger.Logf
}

func (h *Healthz) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	h.Lock()
	defer h.Unlock()

	if h.hasAddrs {
		w.Header().Add(kubetypes.PodIPv4Header, h.podIPv4)
		if _, err := w.Write([]byte("ok")); err != nil {
			http.Error(w, fmt.Sprintf("error writing status: %v", err), http.StatusInternalServerError)
		}
	} else {
		http.Error(w, "node currently has no tailscale IPs", http.StatusServiceUnavailable)
	}
}

func (h *Healthz) Update(healthy bool) {
	h.Lock()
	defer h.Unlock()

	if h.hasAddrs != healthy {
		h.logger("Setting healthy %v", healthy)
	}
	h.hasAddrs = healthy
}

func (h *Healthz) MonitorHealth(ctx context.Context, lc *local.Client) error {
	w, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialNetMap)
	if err != nil {
		return fmt.Errorf("failed to watch IPN bus: %w", err)
	}

	for {
		n, err := w.Next()
		if err != nil {
			return err
		}

		if self := n.SelfChange; self != nil {
			h.Update(len(self.Addresses) != 0)
		}
	}
}

// RegisterHealthHandlers registers a simple health handler at /healthz.
// A containerized tailscale instance is considered healthy if
// it has at least one tailnet IP address.
func RegisterHealthHandlers(mux *http.ServeMux, podIPv4 string, logger logger.Logf) *Healthz {
	h := &Healthz{
		podIPv4: podIPv4,
		logger:  logger,
	}
	mux.Handle("GET /healthz", h)
	return h
}
