// Package healthhttp exposes process-local HTTP health endpoints for services
// whose primary protocol is not HTTP.
package healthhttp

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

	"hyapp/pkg/grpchealth"
)

const (
	statusOK   = "ok"
	statusFail = "fail"
)

// LiveChecker allows services with richer runtime liveness checks, such as
// room-service, to reuse their existing process probes.
type LiveChecker interface {
	Live() []grpchealth.Check
}

// Response is intentionally close to gateway health responses so CLB probes and
// deploy scripts can consume one stable shape across all services.
type Response struct {
	Status  string             `json:"status"`
	Service string             `json:"service"`
	NodeID  string             `json:"node_id"`
	Checks  []grpchealth.Check `json:"checks"`
}

// Server owns the separate HTTP listener used by CLB health checks for gRPC
// processes. It does not replace gRPC health; it makes health visible to CLB.
type Server struct {
	nodeID   string
	checker  grpchealth.Checker
	server   *http.Server
	listener net.Listener
	once     sync.Once
}

// New creates a health HTTP server. Empty addr disables the HTTP health port so
// unit tests and special one-off tools can keep a single listener.
func New(addr string, nodeID string, checker grpchealth.Checker) (*Server, error) {
	addr = strings.TrimSpace(addr)
	if addr == "" {
		return nil, nil
	}
	if checker == nil {
		return nil, errors.New("health checker is required")
	}

	listener, err := net.Listen("tcp", addr)
	if err != nil {
		return nil, err
	}

	s := &Server{
		nodeID:   strings.TrimSpace(nodeID),
		checker:  checker,
		listener: listener,
	}
	mux := http.NewServeMux()
	mux.HandleFunc("/healthz/live", s.live)
	mux.HandleFunc("/healthz/ready", s.ready)
	s.server = &http.Server{
		Handler:           mux,
		ReadHeaderTimeout: 2 * time.Second,
	}

	return s, nil
}

// Run serves health endpoints until Close is called or the listener fails.
func (s *Server) Run() error {
	if s == nil {
		return nil
	}
	err := s.server.Serve(s.listener)
	if errors.Is(err, http.ErrServerClosed) {
		return nil
	}
	return err
}

// Close stops the health endpoint. Readiness should already be failing before
// this is called because the owning service marks its checker draining first.
func (s *Server) Close(ctx context.Context) error {
	if s == nil {
		return nil
	}
	var err error
	s.once.Do(func() {
		err = s.server.Shutdown(ctx)
	})
	return err
}

func (s *Server) live(writer http.ResponseWriter, request *http.Request) {
	checks := []grpchealth.Check{grpchealth.OK("process")}
	if live, ok := s.checker.(LiveChecker); ok {
		// Services with process-local invariants should expose them on live, but
		// live still avoids slow external dependency probes.
		checks = live.Live()
	}
	s.write(writer, checks)
}

func (s *Server) ready(writer http.ResponseWriter, request *http.Request) {
	s.write(writer, s.checker.Ready(request.Context()))
}

func (s *Server) write(writer http.ResponseWriter, checks []grpchealth.Check) {
	status := statusFail
	code := http.StatusServiceUnavailable
	if grpchealth.Healthy(checks) {
		status = statusOK
		code = http.StatusOK
	}
	writer.Header().Set("Content-Type", "application/json")
	writer.WriteHeader(code)
	_ = json.NewEncoder(writer).Encode(Response{
		Status:  status,
		Service: s.checker.ServiceName(),
		NodeID:  s.nodeID,
		Checks:  checks,
	})
}
