// Package proxy 提供反向代理功能，支持 HTTP、WebSocket 和流式代理。
//
// 此文件实现了针对后端目标的健康检查功能，支持
// 主动健康检查（定期 HTTP 探测）和被动健康检查
// （基于观察到的失败标记目标为不健康）。
//
// 主要功能：
//   - 定期向后端发送健康检查请求
//   - 根据响应状态更新目标健康状态
//   - 支持被动健康检查（基于请求失败）
//
// 作者：xfy
//
//go:generate go test -v ./...
package proxy

import (
	"sync"
	"sync/atomic"
	"time"

	"github.com/valyala/fasthttp"
	"rua.plus/lolly/internal/config"
	"rua.plus/lolly/internal/loadbalance"
)

// healthPath 默认健康检查路径。
// 当配置中未指定 path 时使用此值。
const healthPath = "/health"

// HealthChecker 对后端目标执行健康检查。
// 它支持主动（定期 HTTP 探测）和被动（基于失败的）
// 两种健康检查模式。
//
// 当启动后，检查器在后台 goroutine 中运行，定期
// 向每个目标的健康检查端点发送 HTTP GET 请求。
// 返回 2xx 状态码的目标被标记为健康；
// 超时、连接失败或非 2xx 响应将其标记为不健康。
//
// 使用示例：
//
//	targets := []*loadbalance.Target{
//	    {URL: "http://backend1:8080"},
//	    {URL: "http://backend2:8080"},
//	}
//	targets[0].Healthy.Store(true)
//	targets[1].Healthy.Store(true)
//
//	cfg := &config.HealthCheckConfig{
//	    Interval: 10 * time.Second,
//	    Path:     "/health",
//	    Timeout:  5 * time.Second,
//	}
//
//	checker := New(targets, cfg)
//	checker.Start()
//	defer checker.Stop()
type HealthChecker struct {
	stopCh           chan struct{}
	client           *fasthttp.Client
	path             string
	targets          []*loadbalance.Target
	interval         time.Duration
	timeout          time.Duration
	running          atomic.Bool
	matcher          HealthMatch                   // 健康检查匹配器
	slowStartManager *loadbalance.SlowStartManager // 慢启动管理器
	wg               sync.WaitGroup                // 等待 run goroutine 退出
}

// NewHealthChecker 使用指定的目标和配置创建一个新的 HealthChecker。
// 配置定义了检查间隔、超时和健康检查路径。
//
// 如果配置中未指定，将应用默认值：
//   - Interval: 10 秒
//   - Timeout: 5 秒
//   - Path: "/health"
//
// 返回的 HealthChecker 尚未启动；调用 Start() 开始健康检查。
func NewHealthChecker(targets []*loadbalance.Target, cfg *config.HealthCheckConfig) *HealthChecker {
	if cfg == nil {
		cfg = &config.HealthCheckConfig{}
	}
	interval := cfg.Interval
	if interval <= 0 {
		interval = 10 * time.Second
	}

	timeout := cfg.Timeout
	if timeout <= 0 {
		timeout = 5 * time.Second
	}

	path := cfg.Path
	if path == "" {
		path = healthPath
	}

	// 创建健康检查匹配器
	var matcher HealthMatch
	if cfg.Match != nil {
		matcher = NewHealthMatch(&HealthMatchConfig{
			Status:  cfg.Match.Status,
			Body:    cfg.Match.Body,
			Headers: cfg.Match.Headers,
		})
	}
	if matcher == nil {
		matcher = DefaultHealthMatch()
	}

	// 创建慢启动管理器
	var slowStartManager *loadbalance.SlowStartManager
	if cfg.SlowStart > 0 {
		slowStartManager = loadbalance.NewSlowStartManager(cfg.SlowStart)
	}

	h := &HealthChecker{
		targets:          targets,
		interval:         interval,
		timeout:          timeout,
		path:             path,
		stopCh:           make(chan struct{}),
		matcher:          matcher,
		slowStartManager: slowStartManager,
		client: &fasthttp.Client{
			ReadTimeout:  timeout,
			WriteTimeout: timeout,
		},
	}

	// 初始化慢启动管理器的目标查找回调
	if h.slowStartManager != nil {
		h.slowStartManager.SetFindTarget(h.findTargetByURL)
	}

	return h
}

// Start 启动后台健康检查进程。
// 它启动一个 goroutine，按照配置的间隔定期检查所有目标。
// Start 是幂等的；在已运行的检查器上调用它不会产生任何效果。
//
// 健康检查进程将持续运行，直到调用 Stop()。
func (h *HealthChecker) Start() {
	if h.running.Load() {
		return
	}

	h.running.Store(true)
	if h.slowStartManager != nil {
		h.slowStartManager.Start()
	}
	h.wg.Add(1)
	go h.run()
}

// Stop 停止后台健康检查进程。
// 它向后台 goroutine 发送停止信号并等待其完成。
// Stop 是幂等的；在已停止的检查器上调用它不会产生任何效果。
// Stop 后可以再次调用 Start 重新启动检查器。
func (h *HealthChecker) Stop() {
	if !h.running.CompareAndSwap(true, false) {
		return // 已经停止，直接返回
	}
	close(h.stopCh)
	h.wg.Wait() // 等待 run goroutine 退出
	if h.slowStartManager != nil {
		h.slowStartManager.Stop()
	}
	// 重新创建 stopCh 以支持后续 Start
	h.stopCh = make(chan struct{})
}

// run 是在后台 goroutine 中运行的主要健康检查循环。
// 它对所有目标执行初始检查，然后进入循环，
// 以固定间隔检查目标，直到被停止。
func (h *HealthChecker) run() {
	defer h.wg.Done()

	// 执行初始健康检查
	h.checkAll()

	ticker := time.NewTicker(h.interval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			h.checkAll()
		case <-h.stopCh:
			return
		}
	}
}

// checkAll 对所有配置的目标执行健康检查。
// 它使用 goroutines 并发检查每个目标以最小化延迟。
func (h *HealthChecker) checkAll() {
	var wg sync.WaitGroup

	for _, target := range h.targets {
		wg.Add(1)
		go func(t *loadbalance.Target) {
			defer wg.Done()
			h.checkTarget(t)
		}(target)
	}

	wg.Wait()
}

// checkTarget 对单个目标执行健康检查。
// 它向目标的健康检查端点发送 HTTP GET 请求
// 并根据响应更新目标的 Healthy 状态。
//
// 目标被认为健康，如果满足以下条件：
//   - HTTP 请求成功
//   - matcher.Match 返回 true
//
// 目标被标记为不健康，如果满足以下条件：
//   - 连接失败
//   - 请求超时
//   - matcher.Match 返回 false
func (h *HealthChecker) checkTarget(target *loadbalance.Target) {
	// 构建健康检查 URL
	url := target.URL + h.path

	// 准备请求和响应
	req := fasthttp.AcquireRequest()
	resp := fasthttp.AcquireResponse()
	defer fasthttp.ReleaseRequest(req)
	defer fasthttp.ReleaseResponse(resp)

	req.SetRequestURI(url)
	req.Header.SetMethod(fasthttp.MethodGet)
	req.Header.Set("User-Agent", "Lolly-HealthChecker/1.0")

	// 执行带超时的健康检查
	err := h.client.DoTimeout(req, resp, h.timeout)
	if err != nil {
		// 连接失败或超时 - 标记为不健康
		h.MarkUnhealthy(target)
		return
	}

	// 提取响应头（小写 key，预分配容量）
	headers := make(map[string]string, 20)
	for key, value := range resp.Header.All() {
		headers[string(key)] = string(value)
	}

	// 使用 matcher 判断健康状态
	statusCode := resp.StatusCode()
	body := resp.Body()
	if h.matcher.Match(statusCode, body, headers) {
		h.MarkHealthy(target)
	} else {
		h.MarkUnhealthy(target)
	}
}

// MarkUnhealthy 将目标标记为不健康。
// 此方法用于被动健康检查，代理根据请求处理过程中
// 观察到的失败将目标标记为不健康。
//
// 同时调用 RecordFailure 记录软失败状态，配合 MaxFails/FailTimeout
// 实现失败计数和冷却机制。
// 同时通知 SlowStartManager 清除慢启动状态。
func (h *HealthChecker) MarkUnhealthy(target *loadbalance.Target) {
	target.Healthy.Store(false)
	target.RecordFailure()
	if h.slowStartManager != nil {
		h.slowStartManager.OnTargetUnhealthy(target)
	}
}

// MarkHealthy 将目标标记为健康。
// 此方法用于故障转移成功后，将之前失败的目标恢复为健康状态。
//
// 同时调用 RecordSuccess 重置软失败状态（failCount/failedUntil），
// 但不修改 Healthy 标志——健康检查器对 Healthy 拥有权威。
// 同时通知 SlowStartManager 开始慢启动。
func (h *HealthChecker) MarkHealthy(target *loadbalance.Target) {
	target.Healthy.Store(true)
	target.RecordSuccess()
	if h.slowStartManager != nil {
		h.slowStartManager.OnTargetHealthy(target)
	}
}

// findTargetByURL 根据 URL 查找对应的后端目标。
//
// 作为 SlowStartManager 的目标查找回调，用于将慢启动状态
// 同步到负载均衡目标的 EffectiveWeight。
//
// 参数：
//   - url: 后端目标 URL
//
// 返回值：
//   - *loadbalance.Target: 找到的目标，不存在时返回 nil
func (h *HealthChecker) findTargetByURL(url string) *loadbalance.Target {
	for _, t := range h.targets {
		if t.URL == url {
			return t
		}
	}
	return nil
}
