// Package health provides health check functionality for microservices.
//
// It supports Kubernetes-style liveness and readiness probes, along with
// pluggable health checks for dependencies like databases, caches, and
// external services.
//
// Basic usage:
//
//	// Register checks
//	health.Register("database", health.PingCheck(db.Ping))
//	health.Register("redis", health.TCPCheck("localhost:6379", time.Second))
//
//	// Add handlers
//	http.Handle("/health", health.Handler())
//	http.Handle("/health/live", health.LiveHandler())
//	http.Handle("/health/ready", health.ReadyHandler())
//
// Or use the convenience function to register all routes:
//
//	health.RegisterHandlers(mux)
package health

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"net"
	"net/http"
	"runtime"
	"sync"
	"time"

	"go-micro.dev/v6/registry"
)

// Status represents the health status of a check or the overall system
type Status string

const (
	StatusUp   Status = "up"
	StatusDown Status = "down"
)

// CheckFunc is a function that performs a health check.
// It should return nil if healthy, or an error describing the problem.
type CheckFunc func(ctx context.Context) error

// Check represents a registered health check
type Check struct {
	Name     string
	Check    CheckFunc
	Timeout  time.Duration
	Critical bool // If true, failure marks the service as not ready
}

// Result represents the result of a health check
type Result struct {
	Name     string        `json:"name"`
	Status   Status        `json:"status"`
	Error    string        `json:"error,omitempty"`
	Duration time.Duration `json:"duration"`
}

// Response represents the overall health response
type Response struct {
	Status Status            `json:"status"`
	Checks []Result          `json:"checks,omitempty"`
	Info   map[string]string `json:"info,omitempty"`
}

var (
	mu             sync.RWMutex
	checks         []Check
	info           = make(map[string]string)
	defaultTimeout = 5 * time.Second
)

// Register adds a health check with default settings (critical, 5s timeout)
func Register(name string, check CheckFunc) {
	RegisterCheck(Check{
		Name:     name,
		Check:    check,
		Timeout:  defaultTimeout,
		Critical: true,
	})
}

// RegisterCheck adds a health check with custom settings
func RegisterCheck(check Check) {
	if check.Timeout == 0 {
		check.Timeout = defaultTimeout
	}
	mu.Lock()
	checks = append(checks, check)
	mu.Unlock()
}

// SetInfo sets metadata to include in health responses
func SetInfo(key, value string) {
	mu.Lock()
	info[key] = value
	mu.Unlock()
}

// Reset clears all registered checks and info (useful for testing)
func Reset() {
	mu.Lock()
	checks = nil
	info = make(map[string]string)
	mu.Unlock()
}

// Run executes all health checks and returns the results
func Run(ctx context.Context) Response {
	mu.RLock()
	checksCopy := make([]Check, len(checks))
	copy(checksCopy, checks)
	infoCopy := make(map[string]string)
	for k, v := range info {
		infoCopy[k] = v
	}
	mu.RUnlock()

	// Add runtime info
	infoCopy["go_version"] = runtime.Version()
	infoCopy["go_os"] = runtime.GOOS
	infoCopy["go_arch"] = runtime.GOARCH

	if len(checksCopy) == 0 {
		return Response{
			Status: StatusUp,
			Info:   infoCopy,
		}
	}

	// Run checks concurrently
	results := make([]Result, len(checksCopy))
	var wg sync.WaitGroup

	for i, check := range checksCopy {
		wg.Add(1)
		go func(i int, check Check) {
			defer wg.Done()
			results[i] = runCheck(ctx, check)
		}(i, check)
	}

	wg.Wait()

	// Determine overall status
	overallStatus := StatusUp
	for i, result := range results {
		if result.Status == StatusDown && checksCopy[i].Critical {
			overallStatus = StatusDown
			break
		}
	}

	return Response{
		Status: overallStatus,
		Checks: results,
		Info:   infoCopy,
	}
}

func runCheck(ctx context.Context, check Check) Result {
	ctx, cancel := context.WithTimeout(ctx, check.Timeout)
	defer cancel()

	start := time.Now()
	err := check.Check(ctx)
	duration := time.Since(start)

	result := Result{
		Name:     check.Name,
		Status:   StatusUp,
		Duration: duration,
	}

	if err != nil {
		result.Status = StatusDown
		result.Error = err.Error()
	}

	return result
}

// IsReady returns true if all critical checks pass
func IsReady(ctx context.Context) bool {
	resp := Run(ctx)
	return resp.Status == StatusUp
}

// IsLive always returns true (basic liveness)
// Override with SetLivenessCheck for custom behavior
func IsLive() bool {
	return true
}

// Handler returns an http.Handler for the main health endpoint
// Returns 200 if healthy, 503 if unhealthy
func Handler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := Run(r.Context())
		writeResponse(w, resp)
	})
}

// LiveHandler returns an http.Handler for the liveness probe
// Returns 200 if the service is alive (basic check)
func LiveHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if IsLive() {
			w.WriteHeader(http.StatusOK)
			w.Write([]byte(`{"status":"up"}`))
		} else {
			w.WriteHeader(http.StatusServiceUnavailable)
			w.Write([]byte(`{"status":"down"}`))
		}
	})
}

// ReadyHandler returns an http.Handler for the readiness probe
// Returns 200 if all critical checks pass, 503 otherwise
func ReadyHandler() http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		resp := Run(r.Context())
		writeResponse(w, resp)
	})
}

func writeResponse(w http.ResponseWriter, resp Response) {
	w.Header().Set("Content-Type", "application/json")
	if resp.Status == StatusUp {
		w.WriteHeader(http.StatusOK)
	} else {
		w.WriteHeader(http.StatusServiceUnavailable)
	}
	json.NewEncoder(w).Encode(resp)
}

// RegisterHandlers registers all health endpoints on the given mux
func RegisterHandlers(mux *http.ServeMux) {
	mux.Handle("/health", Handler())
	mux.Handle("/health/live", LiveHandler())
	mux.Handle("/health/ready", ReadyHandler())
}

// --- Built-in Checks ---

// PingCheck creates a check from a ping function (like sql.DB.Ping)
func PingCheck(ping func() error) CheckFunc {
	return func(ctx context.Context) error {
		return ping()
	}
}

// PingContextCheck creates a check from a ping function that accepts context
func PingContextCheck(ping func(context.Context) error) CheckFunc {
	return ping
}

// TCPCheck creates a check that verifies TCP connectivity
func TCPCheck(addr string, timeout time.Duration) CheckFunc {
	return func(ctx context.Context) error {
		conn, err := net.DialTimeout("tcp", addr, timeout)
		if err != nil {
			return fmt.Errorf("tcp dial %s: %w", addr, err)
		}
		conn.Close()
		return nil
	}
}

// HTTPCheck creates a check that verifies an HTTP endpoint returns 200
func HTTPCheck(url string, timeout time.Duration) CheckFunc {
	return func(ctx context.Context) error {
		client := &http.Client{Timeout: timeout}
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
		if err != nil {
			return err
		}
		resp, err := client.Do(req)
		if err != nil {
			return fmt.Errorf("http get %s: %w", url, err)
		}
		defer resp.Body.Close()
		if resp.StatusCode != http.StatusOK {
			return fmt.Errorf("http get %s: status %d", url, resp.StatusCode)
		}
		return nil
	}
}

// DNSCheck creates a check that verifies DNS resolution
func DNSCheck(host string) CheckFunc {
	return func(ctx context.Context) error {
		_, err := net.LookupHost(host)
		if err != nil {
			return fmt.Errorf("dns lookup %s: %w", host, err)
		}
		return nil
	}
}

// RegistryCheck creates a check that verifies the service registry is
// reachable by listing services. Registered checks are critical by
// default, so a service is reported not-ready when it loses its
// connection to the registry — wire it to a Kubernetes readiness probe
// on /health/ready:
//
//	health.Register("registry", health.RegistryCheck(service.Options().Registry))
//
// The lookup runs under the check's timeout, so an unreachable registry
// (for example an etcd that has gone away) is reported as down rather
// than blocking the probe. Registries that honor ListContext also receive
// the check context directly.
func RegistryCheck(reg registry.Registry) CheckFunc {
	return func(ctx context.Context) error {
		if reg == nil {
			return errors.New("no registry configured")
		}
		errc := make(chan error, 1)
		go func() {
			_, err := reg.ListServices(registry.ListContext(ctx))
			errc <- err
		}()
		select {
		case err := <-errc:
			if err != nil {
				return fmt.Errorf("registry %s unreachable: %w", reg.String(), err)
			}
			return nil
		case <-ctx.Done():
			return fmt.Errorf("registry %s check timed out: %w", reg.String(), ctx.Err())
		}
	}
}

// RegistryServiceCheck creates a check that verifies the local service
// registration is visible in the registry. RegistryCheck only proves the
// registry is reachable; this check also catches the common failure mode
// where the process is alive and the registry answers, but this service's
// node lease/record has disappeared and other services can no longer
// discover it.
//
// Pass the service name and node ID used during registration. The default
// RPC server node ID is service.Options().Name + "-" + service.Options().Id.
func RegistryServiceCheck(reg registry.Registry, serviceName, nodeID string) CheckFunc {
	return func(ctx context.Context) error {
		if reg == nil {
			return errors.New("no registry configured")
		}
		if serviceName == "" {
			return errors.New("no service name configured")
		}
		if nodeID == "" {
			return errors.New("no service node configured")
		}

		errc := make(chan error, 1)
		go func() {
			services, err := reg.GetService(serviceName, registry.GetContext(ctx))
			if err != nil {
				errc <- fmt.Errorf("registry %s lookup %s failed: %w", reg.String(), serviceName, err)
				return
			}
			for _, service := range services {
				for _, node := range service.Nodes {
					if node.Id == nodeID {
						errc <- nil
						return
					}
				}
			}
			errc <- fmt.Errorf("registry %s missing node %s for service %s", reg.String(), nodeID, serviceName)
		}()

		select {
		case err := <-errc:
			return err
		case <-ctx.Done():
			return fmt.Errorf("registry %s service check timed out: %w", reg.String(), ctx.Err())
		}
	}
}

// CustomCheck creates a check from any function returning an error
func CustomCheck(fn func() error) CheckFunc {
	return func(ctx context.Context) error {
		return fn()
	}
}
