// Package healthcheck 定义 room-service 的 gRPC health 探针状态和依赖检查。
package healthcheck

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

	"hyapp/pkg/grpchealth"
)

const (
	// serviceName 是标准 gRPC health 对外暴露的服务名。
	serviceName = "room-service"

	// readyBudget 限制 ready 总耗时，避免探针堆积。
	readyBudget = 500 * time.Millisecond
	// mysqlBudget 限制 MySQL ping 时间，MySQL 不可用时不能接新房间命令。
	mysqlBudget = 300 * time.Millisecond
	// redisBudget 限制 Redis ping 和 lease probe 时间。
	redisBudget = 200 * time.Millisecond
	// redisLeaseTTL 是健康检查 lease 探针使用的短 TTL。
	redisLeaseTTL = 2 * time.Second
)

// RuntimeProbe 覆盖 Room Cell owner、repository 和 directory 等进程内装配状态。
type RuntimeProbe interface {
	// HealthCheck 只检查进程内对象是否装配完整，不做慢 IO。
	HealthCheck() error
}

// MySQLPinger 抽象 MySQL repository 的轻量连通性检查。
type MySQLPinger interface {
	// Ping 验证 MySQL 连接池可用，调用方负责传入短 deadline。
	Ping(ctx context.Context) error
}

// RedisProbe 抽象 Redis 当前可访问性和 lease Lua 原子路径探测。
type RedisProbe interface {
	// Ping 验证 Redis 基础连通性。
	Ping(ctx context.Context) error
	// ProbeLease 验证 Redis lease Lua 原子路径可用。
	ProbeLease(ctx context.Context, nodeID string, now time.Time, ttl time.Duration) error
}

// State 记录 room-service readiness 所需的进程状态和硬依赖。
type State struct {
	// nodeID 是当前 room-service 实例标识，Redis lease 探针需要它。
	nodeID string
	// runtime 是领域服务的进程内装配状态。
	runtime RuntimeProbe
	// mysql 是持久化连通性检查对象。
	mysql MySQLPinger
	// redis 是路由目录连通性和 lease 脚本检查对象。
	redis RedisProbe
	// grpcActive 表示 gRPC listener 已进入 Serve 循环。
	grpcActive atomic.Bool
	// draining 表示节点正在下线，ready 必须失败以停止接新命令和接管。
	draining atomic.Bool
}

// NewState 创建 room-service 健康状态所有者。
func NewState(nodeID string, runtime RuntimeProbe, mysql MySQLPinger, redis RedisProbe) *State {
	return &State{
		nodeID:  nodeID,
		runtime: runtime,
		mysql:   mysql,
		redis:   redis,
	}
}

// ServiceName 返回标准 gRPC health 使用的服务边界名。
func (s *State) ServiceName() string {
	return serviceName
}

// MarkGRPCServing 标记 gRPC listener 已进入 Serve 循环。
func (s *State) MarkGRPCServing() {
	s.grpcActive.Store(true)
}

// MarkGRPCStopped 标记 gRPC listener 已退出 Serve 循环。
func (s *State) MarkGRPCStopped() {
	s.grpcActive.Store(false)
}

// MarkDraining 让 ready 立即失败，避免下线节点继续接管新房间。
func (s *State) MarkDraining() {
	s.draining.Store(true)
}

// Live 检查本进程关键内存结构，不把 MySQL/Redis 抖动映射为 live 失败。
func (s *State) Live() []grpchealth.Check {
	// live 只判断进程是否应该继续存在，不因为 MySQL/Redis 短抖动重启进程。
	checks := []grpchealth.Check{
		grpchealth.Bool("grpc_server", s.grpcActive.Load(), "gRPC listener is not serving"),
	}

	checks = append(checks, s.runtimeCheck())
	return checks
}

// Ready 检查 room-service 接新命令所需的硬依赖。
func (s *State) Ready(ctx context.Context) []grpchealth.Check {
	// ready 是流量入口判定，必须包含持久化、Redis lease 和 drain 状态。
	ctx, cancel := context.WithTimeout(ctx, readyBudget)
	defer cancel()

	checks := []grpchealth.Check{
		grpchealth.Bool("grpc_listener", s.grpcActive.Load(), "gRPC listener is not serving"),
		grpchealth.Bool("draining", !s.draining.Load(), "node is draining"),
	}

	checks = append(checks, s.runtimeCheck())
	checks = append(checks, s.mysqlCheck(ctx))
	checks = append(checks, s.redisPingCheck(ctx))
	checks = append(checks, s.redisLeaseCheck(ctx))

	return checks
}

func (s *State) runtimeCheck() grpchealth.Check {
	if s.runtime == nil {
		// runtime 缺失说明 App.New 装配异常。
		return grpchealth.Fail("room_runtime", "runtime probe is not initialized")
	}
	if err := s.runtime.HealthCheck(); err != nil {
		// 进程内核心对象缺失时不能处理任何房间命令。
		return grpchealth.Fail("room_runtime", err.Error())
	}

	return grpchealth.OK("room_runtime")
}

func (s *State) mysqlCheck(ctx context.Context) grpchealth.Check {
	if s.mysql == nil {
		// 没有 MySQL repository 时无法保存 command log/snapshot/outbox。
		return grpchealth.Fail("mysql", "MySQL repository is not initialized")
	}

	checkCtx, cancel := context.WithTimeout(ctx, mysqlBudget)
	defer cancel()
	if err := s.mysql.Ping(checkCtx); err != nil {
		// MySQL 不可达时即使内存 Cell 存在，也不能安全接新写命令。
		return grpchealth.Fail("mysql", err.Error())
	}

	return grpchealth.OK("mysql")
}

func (s *State) redisPingCheck(ctx context.Context) grpchealth.Check {
	if s.redis == nil {
		// 没有 Redis directory 时无法保证单房间单活。
		return grpchealth.Fail("redis_ping", "Redis probe is not initialized")
	}

	checkCtx, cancel := context.WithTimeout(ctx, redisBudget)
	defer cancel()
	if err := s.redis.Ping(checkCtx); err != nil {
		// Redis 不可达时新房间接管和续租语义不可用。
		return grpchealth.Fail("redis_ping", err.Error())
	}

	return grpchealth.OK("redis_ping")
}

func (s *State) redisLeaseCheck(ctx context.Context) grpchealth.Check {
	if s.redis == nil {
		// lease 探针和 ping 分开，方便定位是连通性还是 Lua 原子路径问题。
		return grpchealth.Fail("redis_lease", "Redis probe is not initialized")
	}

	checkCtx, cancel := context.WithTimeout(ctx, redisBudget)
	defer cancel()
	if err := s.redis.ProbeLease(checkCtx, s.nodeID, time.Now().UTC(), redisLeaseTTL); err != nil {
		// Eval/cjson/写权限异常会直接破坏 owner 仲裁，ready 必须失败。
		return grpchealth.Fail("redis_lease", err.Error())
	}

	return grpchealth.OK("redis_lease")
}
