// Package router 维护 room-service 的房间 owner 路由目录和 lease 语义。
package router

import (
	"context"
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
	"time"

	"github.com/redis/go-redis/v9"
	"hyapp/pkg/idgen"
)

// roomRouteKeyPrefix 是生产房间 owner lease 的 Redis key 前缀。
const roomRouteKeyPrefix = "room:route:"

// roomNodeKeyPrefix 保存 room-service 节点的实例直连地址，owner 转发只读取该短租约。
const roomNodeKeyPrefix = "room:node:"

// healthLeaseKey 是 ready 探针专用 key，避免探针污染真实房间路由。
const healthLeaseKey = "room:healthcheck:lease"

const ensureOwnerScript = `
local current = redis.call("GET", KEYS[1])
if not current then
	redis.call("SET", KEYS[1], ARGV[3], "PX", ARGV[2])
	return ARGV[3]
end

local ok, decoded = pcall(cjson.decode, current)
if not ok then
	redis.call("SET", KEYS[1], ARGV[3], "PX", ARGV[2])
	return ARGV[3]
end

local expires_at_ms = tonumber(decoded["expires_at_ms"]) or 0
if decoded["node_id"] == ARGV[4] and expires_at_ms > tonumber(ARGV[1]) then
	local next_ok, next_decoded = pcall(cjson.decode, ARGV[3])
	if not next_ok then
		redis.call("SET", KEYS[1], ARGV[3], "PX", ARGV[2])
		return ARGV[3]
	end
	decoded["room_id"] = next_decoded["room_id"]
	decoded["expires_at_ms"] = next_decoded["expires_at_ms"]
	local refreshed = cjson.encode(decoded)
	redis.call("SET", KEYS[1], refreshed, "PX", ARGV[2])
	return refreshed
end

if expires_at_ms <= tonumber(ARGV[1]) then
	redis.call("SET", KEYS[1], ARGV[3], "PX", ARGV[2])
	return ARGV[3]
end

return current
`

const verifyOwnerScript = `
local current = redis.call("GET", KEYS[1])
if not current then
	return 0
end

local ok, decoded = pcall(cjson.decode, current)
if not ok then
	return 0
end

if decoded["node_id"] == ARGV[2] and decoded["lease_token"] == ARGV[3] and tonumber(decoded["expires_at_ms"]) > tonumber(ARGV[1]) then
	return 1
end

return 0
`

const releaseOwnerScript = `
local current = redis.call("GET", KEYS[1])
if not current then
	return 0
end

local ok, decoded = pcall(cjson.decode, current)
if not ok then
	return 0
end

if decoded["node_id"] == ARGV[1] then
	redis.call("DEL", KEYS[1])
	return 1
end

return 0
`

// RedisDirectory 用 Redis 保存 room_id 到执行节点的带 TTL 租约。
type RedisDirectory struct {
	// client 是 go-redis 客户端，连接生命周期由 app.App 关闭。
	client *redis.Client
}

// redisLeaseValue 是 Redis JSON 载荷，字段名稳定便于排查和跨语言工具读取。
type redisLeaseValue struct {
	RoomID     string `json:"room_id"`
	NodeID     string `json:"node_id"`
	LeaseToken string `json:"lease_token"`
	ExpiresAt  int64  `json:"expires_at_ms"`
}

// redisNodeValue 是 Redis 中的节点注册 JSON，字段保持可读以便 redis-cli 排查。
type redisNodeValue struct {
	NodeID    string `json:"node_id"`
	GRPCAddr  string `json:"grpc_addr"`
	UpdatedAt int64  `json:"updated_at_ms"`
	ExpiresAt int64  `json:"expires_at_ms"`
}

// NewRedisDirectory 创建 Redis 目录实现。
func NewRedisDirectory(client *redis.Client) *RedisDirectory {
	return &RedisDirectory{client: client}
}

// NewRedisClient 创建 Redis 客户端，并验证连接可用。
func NewRedisClient(ctx context.Context, addr string, password string, db int) (*redis.Client, error) {
	// Redis 只保存热路由/lease，不作为房间恢复的唯一来源。
	client := redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: password,
		DB:       db,
	})

	if err := client.Ping(ctx).Err(); err != nil {
		// 启动探测失败时关闭客户端，避免调用方遗留半初始化连接。
		_ = client.Close()
		return nil, err
	}

	return client, nil
}

// Ping 验证 Redis 当前可访问，调用方负责传入短 deadline。
func (d *RedisDirectory) Ping(ctx context.Context) error {
	// ready 探针调用该方法确认 Redis 基础连通性。
	return d.client.Ping(ctx).Err()
}

// ProbeLease 使用与房间归属相同的 Lua 路径探测 Redis lease 原子操作能力。
func (d *RedisDirectory) ProbeLease(ctx context.Context, nodeID string, now time.Time, ttl time.Duration) error {
	// 探针必须走同一段 Lua，才能发现 Eval 权限或 cjson 支持问题。
	next := Lease{
		RoomID:     "__healthcheck__",
		NodeID:     nodeID,
		LeaseToken: idgen.New("lease"),
		ExpiresAt:  now.Add(ttl),
	}

	payload, err := encodeLease(next)
	if err != nil {
		return err
	}

	// Lua 脚本保证 read-check-set 原子化，避免 GET + SET 竞争导致双 owner。
	value, err := d.client.Eval(ctx,
		ensureOwnerScript,
		[]string{healthLeaseKey},
		strconv.FormatInt(now.UnixMilli(), 10),
		strconv.FormatInt(ttl.Milliseconds(), 10),
		payload,
		nodeID,
	).Text()
	if err != nil {
		return err
	}

	// 返回值可能是已有租约或新租约，能解码即可证明脚本路径正常。
	_, err = decodeLease(value)
	return err
}

// Lookup 查询房间当前 owner 租约。
func (d *RedisDirectory) Lookup(ctx context.Context, roomID string) (Lease, bool, error) {
	value, err := d.client.Get(ctx, routeKey(roomID)).Result()
	if err != nil {
		if err == redis.Nil {
			// key 不存在表示没有当前 owner。
			return Lease{}, false, nil
		}

		return Lease{}, false, err
	}

	lease, err := decodeLease(value)
	if err != nil {
		// Redis 值损坏时不能静默接管，否则可能破坏单活。
		return Lease{}, false, err
	}

	return lease, true, nil
}

// EnsureOwner 在租约不存在、过期或归当前节点时授予本节点执行权。
func (d *RedisDirectory) EnsureOwner(ctx context.Context, roomID string, nodeID string, now time.Time, ttl time.Duration) (Lease, error) {
	// next 是当前节点希望写入的租约；Lua 会决定是否真正采用它。
	// 同节点续租时 Lua 会保留现有 lease_token，只刷新 expires_at_ms，
	// 避免后台 worker 续租把正在执行的同节点命令误判为旧 owner。
	next := Lease{
		RoomID:     roomID,
		NodeID:     nodeID,
		LeaseToken: idgen.New("lease"),
		ExpiresAt:  now.Add(ttl),
	}

	payload, err := encodeLease(next)
	if err != nil {
		return Lease{}, err
	}

	// Redis TTL 和 payload.expires_at_ms 同时写入，TTL 用于自动清理，payload 用于脚本判断。
	value, err := d.client.Eval(ctx,
		ensureOwnerScript,
		[]string{routeKey(roomID)},
		strconv.FormatInt(now.UnixMilli(), 10),
		strconv.FormatInt(ttl.Milliseconds(), 10),
		payload,
		nodeID,
	).Text()
	if err != nil {
		return Lease{}, err
	}

	lease, err := decodeLease(value)
	if err != nil {
		// 返回值无法解码时调用方不能确认 owner，必须失败。
		return Lease{}, err
	}

	return lease, nil
}

// VerifyOwner 校验调用方持有的 lease token 仍是当前有效 owner。
func (d *RedisDirectory) VerifyOwner(ctx context.Context, roomID string, nodeID string, leaseToken string, now time.Time) (bool, error) {
	value, err := d.client.Eval(ctx,
		verifyOwnerScript,
		[]string{routeKey(roomID)},
		strconv.FormatInt(now.UnixMilli(), 10),
		nodeID,
		leaseToken,
	).Int()
	if err != nil {
		return false, err
	}

	return value == 1, nil
}

// ReleaseOwner 在节点安全下线后删除自己持有的 route lease。
func (d *RedisDirectory) ReleaseOwner(ctx context.Context, roomID string, nodeID string) (bool, error) {
	value, err := d.client.Eval(ctx, releaseOwnerScript, []string{routeKey(roomID)}, nodeID).Int()
	if err != nil {
		return false, err
	}

	return value == 1, nil
}

// ForceExpire 只服务测试，生产启动路径不会调用。
func (d *RedisDirectory) ForceExpire(roomID string) {
	// Redis 实现直接删除 key 来模拟 lease 过期，避免等待真实 TTL。
	_ = d.client.Del(context.Background(), routeKey(roomID)).Err()
}

// RegisterNode 写入当前 room-service 实例地址；TTL 过期后 owner 转发不会再使用该节点。
func (d *RedisDirectory) RegisterNode(ctx context.Context, node NodeRegistration, ttl time.Duration) error {
	if ttl <= 0 {
		ttl = 30 * time.Second
	}
	node.NodeID = strings.TrimSpace(node.NodeID)
	node.GRPCAddr = strings.TrimSpace(node.GRPCAddr)
	if node.NodeID == "" || node.GRPCAddr == "" {
		return fmt.Errorf("node_id and grpc_addr are required")
	}
	now := time.Now().UTC()
	node.UpdatedAt = now
	node.ExpiresAt = now.Add(ttl)
	payload, err := encodeNode(node)
	if err != nil {
		return err
	}

	return d.client.Set(ctx, nodeKey(node.NodeID), payload, ttl).Err()
}

// LookupNode 查询 owner node_id 对应的实例地址。
func (d *RedisDirectory) LookupNode(ctx context.Context, nodeID string) (NodeRegistration, bool, error) {
	value, err := d.client.Get(ctx, nodeKey(nodeID)).Result()
	if err != nil {
		if err == redis.Nil {
			return NodeRegistration{}, false, nil
		}

		return NodeRegistration{}, false, err
	}

	node, err := decodeNode(value)
	if err != nil {
		return NodeRegistration{}, false, err
	}
	if !node.ValidAt(time.Now().UTC()) {
		return NodeRegistration{}, false, nil
	}

	return node, true, nil
}

// UnregisterNode 删除节点注册；下线节点停止接受 owner 转发前必须调用。
func (d *RedisDirectory) UnregisterNode(ctx context.Context, nodeID string) error {
	return d.client.Del(ctx, nodeKey(nodeID)).Err()
}

func routeKey(roomID string) string {
	// 路由 key 只由房间 ID 决定，所有节点必须计算出同一个 Redis key。
	return roomRouteKeyPrefix + roomID
}

func nodeKey(nodeID string) string {
	// 节点 key 使用 node_id，不包含 app_code；一个实例可以服务多个 App 的房间。
	return roomNodeKeyPrefix + strings.TrimSpace(nodeID)
}

func encodeLease(lease Lease) (string, error) {
	// 使用 JSON 而不是二进制，方便线上通过 redis-cli 直接排查 owner。
	payload, err := json.Marshal(redisLeaseValue{
		RoomID:     lease.RoomID,
		NodeID:     lease.NodeID,
		LeaseToken: lease.LeaseToken,
		ExpiresAt:  lease.ExpiresAt.UnixMilli(),
	})
	if err != nil {
		return "", err
	}

	return string(payload), nil
}

func encodeNode(node NodeRegistration) (string, error) {
	payload, err := json.Marshal(redisNodeValue{
		NodeID:    strings.TrimSpace(node.NodeID),
		GRPCAddr:  strings.TrimSpace(node.GRPCAddr),
		UpdatedAt: node.UpdatedAt.UnixMilli(),
		ExpiresAt: node.ExpiresAt.UnixMilli(),
	})
	if err != nil {
		return "", err
	}

	return string(payload), nil
}

func decodeLease(value string) (Lease, error) {
	var payload redisLeaseValue
	if err := json.Unmarshal([]byte(value), &payload); err != nil {
		// 损坏值直接返回错误，让上层 ready 或命令路径暴露问题。
		return Lease{}, err
	}

	// Redis 中保存毫秒时间戳，恢复为 time.Time 便于 ValidAt 判断。
	return Lease{
		RoomID:     payload.RoomID,
		NodeID:     payload.NodeID,
		LeaseToken: payload.LeaseToken,
		ExpiresAt:  time.UnixMilli(payload.ExpiresAt),
	}, nil
}

func decodeNode(value string) (NodeRegistration, error) {
	var payload redisNodeValue
	if err := json.Unmarshal([]byte(value), &payload); err != nil {
		return NodeRegistration{}, err
	}

	return NodeRegistration{
		NodeID:    strings.TrimSpace(payload.NodeID),
		GRPCAddr:  strings.TrimSpace(payload.GRPCAddr),
		UpdatedAt: time.UnixMilli(payload.UpdatedAt),
		ExpiresAt: time.UnixMilli(payload.ExpiresAt),
	}, nil
}
