package auth

import (
	"context"
	"errors"
	"time"

	"hyapp/pkg/xerr"
)

const (
	sessionDenylistWorkerLockTTL     = 15 * time.Second
	sessionDenylistRetryDelay        = 2 * time.Second
	sessionDenylistRehydrateInterval = time.Hour
)

// ProcessSessionDenylistBatch 把 family revoke 同事务写出的 MySQL 事实补偿到 gateway Redis denylist。
// Redis 暂时不可用时 job 会回到 retryable；进程崩溃后 processing lease 到期也会被其他实例重新认领。
func (s *Service) ProcessSessionDenylistBatch(ctx context.Context, workerID string, batchSize int) (int, error) {
	if s.authRepository == nil || s.ipDecisionCache == nil {
		return 0, xerr.New(xerr.Unavailable, "session denylist worker is unavailable")
	}
	if batchSize <= 0 {
		batchSize = 100
	}
	now := s.now()
	jobs, err := s.authRepository.ClaimSessionDenylistJobs(ctx, workerID, now.UnixMilli(), now.Add(sessionDenylistWorkerLockTTL).UnixMilli(), batchSize)
	if err != nil {
		return 0, err
	}
	var batchErr error
	for _, job := range jobs {
		if err := s.writeRevokedAccessSessionUntil(ctx, job.AppCode, job.SessionID, job.Reason, job.DenyUntilMs); err != nil {
			nextRetryAtMs := now.Add(sessionDenylistRetryDelay).UnixMilli()
			if nextRetryAtMs > job.DenyUntilMs {
				nextRetryAtMs = job.DenyUntilMs
			}
			markErr := s.authRepository.MarkSessionDenylistJobRetryable(ctx, job.JobID, err.Error(), nextRetryAtMs, now.UnixMilli())
			batchErr = errors.Join(batchErr, err, markErr)
			continue
		}
		nextRehydrateAtMs := now.Add(sessionDenylistRehydrateInterval).UnixMilli()
		if nextRehydrateAtMs > job.DenyUntilMs {
			nextRehydrateAtMs = job.DenyUntilMs
		}
		if err := s.authRepository.MarkSessionDenylistJobDelivered(ctx, job.JobID, job.AppCode, job.SessionID, job.Reason, job.DenyUntilMs, now.UnixMilli(), nextRehydrateAtMs); err != nil {
			// Redis 已成功；DB 标记失败只会导致幂等重放，不会削弱安全性。
			batchErr = errors.Join(batchErr, err)
		}
	}
	return len(jobs), batchErr
}

// CleanupExpiredSessionDenylistJobs 只由低频维护 ticker 调用，避免空队列时每秒给 MySQL 增加 DELETE QPS。
// 多实例可同时执行：DELETE 使用 (deny_until_ms, job_id) 索引和 LIMIT；到期后所有状态都已无有效 JWT 需要保护。
func (s *Service) CleanupExpiredSessionDenylistJobs(ctx context.Context, batchSize int) (int, error) {
	if s.authRepository == nil {
		return 0, xerr.New(xerr.Unavailable, "session denylist repository is unavailable")
	}
	if batchSize <= 0 {
		batchSize = 1000
	}
	// 清理只信任每个 job 在撤销事务内固化的 deny_until，未来配置下调不能提前删除历史 7d JWT 的保护事实。
	return s.authRepository.DeleteExpiredSessionDenylistJobs(ctx, s.now().UnixMilli(), batchSize)
}

// CleanupExpiredRefreshOutcomes 把短期 token-pair 密文清理移出 refresh 热路径；严格走 expires_at 索引和 LIMIT。
func (s *Service) CleanupExpiredRefreshOutcomes(ctx context.Context, batchSize int) (int, error) {
	if s.authRepository == nil {
		return 0, xerr.New(xerr.Unavailable, "refresh outcome repository is unavailable")
	}
	if batchSize <= 0 {
		batchSize = 1000
	}
	return s.authRepository.DeleteExpiredRefreshOutcomes(ctx, s.now().UnixMilli(), batchSize)
}

// RehydrateAllSessionDenylistJobs 按固定 max job_id 分页直读全部未过期事实并同步写 Redis。
// 它忽略 status/lease，不依赖旧 pod 是否 claim；固定上界和单调 cursor 让 >1000 jobs 也有限终止。
func (s *Service) RehydrateAllSessionDenylistJobs(ctx context.Context, batchSize int) (int, error) {
	if s.authRepository == nil || s.ipDecisionCache == nil {
		return 0, xerr.New(xerr.Unavailable, "session denylist repository is unavailable")
	}
	if batchSize <= 0 {
		batchSize = 1000
	}
	throughJobID, err := s.authRepository.MaxSessionDenylistJobID(ctx)
	if err != nil {
		return 0, err
	}
	nowMs := s.now().UnixMilli()
	var cursor int64
	total := 0
	for cursor < throughJobID {
		jobs, nextCursor, err := s.authRepository.ListSessionDenylistJobsForHydration(ctx, nowMs, cursor, throughJobID, batchSize)
		if err != nil {
			return total, err
		}
		if len(jobs) == 0 || nextCursor <= cursor {
			break
		}
		for _, job := range jobs {
			if err := s.writeRevokedAccessSessionUntil(ctx, job.AppCode, job.SessionID, job.Reason, job.DenyUntilMs); err != nil {
				return total, err
			}
			nextRehydrateAtMs := nowMs + sessionDenylistRehydrateInterval.Milliseconds()
			if nextRehydrateAtMs > job.DenyUntilMs {
				nextRehydrateAtMs = job.DenyUntilMs
			}
			// CAS 只在 job 未被并发延长时标 delivered；延长后的 pending 事实仍由普通 worker 写更长 TTL。
			if err := s.authRepository.MarkSessionDenylistJobDelivered(ctx, job.JobID, job.AppCode, job.SessionID, job.Reason, job.DenyUntilMs, nowMs, nextRehydrateAtMs); err != nil {
				return total, err
			}
			total++
		}
		cursor = nextCursor
	}
	return total, nil
}
