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

import (
	"context"
	"sync"
	"time"

	"hyapp/pkg/idgen"
)

// Lease 表达房间当前执行权归属。
type Lease struct {
	// RoomID 是租约所属房间。
	RoomID string
	// NodeID 是当前拥有房间执行权的 room-service 实例。
	NodeID string
	// LeaseToken 是租约实例标识，便于未来做续约和 fencing。
	LeaseToken string
	// ExpiresAt 是租约过期时间，过期后其他节点可以接管并恢复房间。
	ExpiresAt time.Time
}

// NodeRegistration 记录一个 room-service 实例可被其他实例直连的内网地址。
type NodeRegistration struct {
	// NodeID 必须和 Redis lease 中的 owner node_id 完全一致。
	NodeID string
	// GRPCAddr 是实例私网 gRPC 地址，必须指向单个实例，不能写 room-service 负载均衡地址。
	GRPCAddr string
	// UpdatedAt 是最近一次心跳写入时间，便于排查节点注册是否卡住。
	UpdatedAt time.Time
	// ExpiresAt 是节点注册过期时间；过期节点不能作为 owner 转发目标。
	ExpiresAt time.Time
}

// ValidAt 判断节点注册在指定时间是否仍可作为转发目标。
func (n NodeRegistration) ValidAt(now time.Time) bool {
	// 没有地址或过期时间的注册不能被使用，避免把请求转发到空地址或旧实例。
	return n.NodeID != "" && n.GRPCAddr != "" && !n.ExpiresAt.IsZero() && now.Before(n.ExpiresAt)
}

// ValidAt 判断租约在指定时刻是否仍有效。
func (l Lease) ValidAt(now time.Time) bool {
	// 零值过期时间永远无效，避免空 lease 被误认为可继续持有。
	return !l.ExpiresAt.IsZero() && now.Before(l.ExpiresAt)
}

// Directory 抽象 room_id -> node_id 的房间归属目录。
type Directory interface {
	// Lookup 查询当前记录的房间 owner，不会主动接管。
	Lookup(ctx context.Context, roomID string) (Lease, bool, error)
	// EnsureOwner 在无 owner、owner 过期或 owner 是当前节点时授予当前节点执行权。
	EnsureOwner(ctx context.Context, roomID string, nodeID string, now time.Time, ttl time.Duration) (Lease, error)
	// VerifyOwner 校验当前节点和 lease token 仍然持有执行权，不续租也不接管。
	VerifyOwner(ctx context.Context, roomID string, nodeID string, leaseToken string, now time.Time) (bool, error)
	// ReleaseOwner 在节点安全下线后释放自己仍持有的 owner lease，让新节点无需等待 TTL 才能恢复。
	ReleaseOwner(ctx context.Context, roomID string, nodeID string) (bool, error)
	// ForceExpire 只用于测试模拟租约过期。
	ForceExpire(roomID string)
}

// NodeRegistry 维护 room-service node_id 到实例私网 gRPC 地址的短租约。
type NodeRegistry interface {
	// RegisterNode 写入或续约当前节点地址；ttl 过期后其他节点不能继续转发到该实例。
	RegisterNode(ctx context.Context, node NodeRegistration, ttl time.Duration) error
	// LookupNode 查询一个 owner node_id 的实例地址。
	LookupNode(ctx context.Context, nodeID string) (NodeRegistration, bool, error)
	// UnregisterNode 在实例安全退出后删除注册，避免转发到已停止进程。
	UnregisterNode(ctx context.Context, nodeID string) error
}

// MemoryDirectory 是首版测试与本地运行使用的内存目录实现。
type MemoryDirectory struct {
	// mu 保护 leases，测试里可能用多个服务实例模拟接管。
	mu sync.Mutex
	// leases 保存 room_id -> lease。
	leases map[string]Lease
	// nodes 保存 node_id -> 注册地址，供 transport 层 owner 转发测试复用。
	nodes map[string]NodeRegistration
}

// NewMemoryDirectory 初始化空的内存目录。
func NewMemoryDirectory() *MemoryDirectory {
	return &MemoryDirectory{
		leases: make(map[string]Lease),
		nodes:  make(map[string]NodeRegistration),
	}
}

// Lookup 查询当前房间归属。
func (d *MemoryDirectory) Lookup(_ context.Context, roomID string) (Lease, bool, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	// 内存实现不主动清理过期 lease，EnsureOwner 会在写路径判断有效性。
	lease, ok := d.leases[roomID]

	return lease, ok, nil
}

// EnsureOwner 在租约不存在、已过期或已归当前节点时，授予本节点执行权。
func (d *MemoryDirectory) EnsureOwner(_ context.Context, roomID string, nodeID string, now time.Time, ttl time.Duration) (Lease, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	current, ok := d.leases[roomID]
	if ok && current.ValidAt(now) && current.NodeID != nodeID {
		// 其他节点持有有效 lease 时返回现有 owner，调用方应拒绝执行房间命令。
		return current, nil
	}

	if ok && current.ValidAt(now) && current.NodeID == nodeID {
		// 同一节点续租只能延长过期时间，不能轮换 token。
		// Room Cell 命令在进入 mailbox 前拿到 lease，提交前会用 token 做 fencing；
		// 如果后台 sweep 续租时换 token，就会把同节点正在执行的命令误判为旧 owner。
		current.ExpiresAt = now.Add(ttl)
		current.RoomID = roomID
		d.leases[roomID] = current
		return current, nil
	}

	// 没有 lease 或 lease 已过期时生成新 token；这代表新的执行权实例。
	next := Lease{
		RoomID:     roomID,
		NodeID:     nodeID,
		LeaseToken: idgen.New("lease"),
		ExpiresAt:  now.Add(ttl),
	}

	d.leases[roomID] = next

	return next, nil
}

// VerifyOwner 校验调用方手里的 lease token 仍是当前有效 owner。
func (d *MemoryDirectory) VerifyOwner(_ context.Context, roomID string, nodeID string, leaseToken string, now time.Time) (bool, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	current, ok := d.leases[roomID]
	if !ok {
		return false, nil
	}

	return current.NodeID == nodeID && current.LeaseToken == leaseToken && current.ValidAt(now), nil
}

// ReleaseOwner 删除当前节点持有的内存 lease；其他节点持有时保持不变。
func (d *MemoryDirectory) ReleaseOwner(_ context.Context, roomID string, nodeID string) (bool, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	current, ok := d.leases[roomID]
	if !ok || current.NodeID != nodeID {
		return false, nil
	}

	delete(d.leases, roomID)
	return true, nil
}

// ForceExpire 只用于测试时模拟租约自然过期。
func (d *MemoryDirectory) ForceExpire(roomID string) {
	d.mu.Lock()
	defer d.mu.Unlock()

	lease, ok := d.leases[roomID]
	if !ok {
		// 没有租约时无需处理，测试可安全重复调用。
		return
	}

	lease.ExpiresAt = time.Unix(0, 0)
	d.leases[roomID] = lease
}

// RegisterNode 写入测试用节点地址。
func (d *MemoryDirectory) RegisterNode(_ context.Context, node NodeRegistration, ttl time.Duration) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	now := time.Now().UTC()
	if ttl <= 0 {
		ttl = 30 * time.Second
	}
	node.UpdatedAt = now
	node.ExpiresAt = now.Add(ttl)
	d.nodes[node.NodeID] = node
	return nil
}

// LookupNode 查询测试用节点地址，过期注册按不存在处理。
func (d *MemoryDirectory) LookupNode(_ context.Context, nodeID string) (NodeRegistration, bool, error) {
	d.mu.Lock()
	defer d.mu.Unlock()

	node, ok := d.nodes[nodeID]
	if !ok || !node.ValidAt(time.Now().UTC()) {
		return NodeRegistration{}, false, nil
	}

	return node, true, nil
}

// UnregisterNode 删除测试用节点地址。
func (d *MemoryDirectory) UnregisterNode(_ context.Context, nodeID string) error {
	d.mu.Lock()
	defer d.mu.Unlock()

	delete(d.nodes, nodeID)
	return nil
}
