package auth

import (
	"context"
	"database/sql"
	"strings"

	"hyapp/pkg/appcode"
	authdomain "hyapp/services/user-service/internal/domain/auth"
)

func enqueueSessionDenylistJobs(ctx context.Context, tx *sql.Tx, appCode string, sessionIDs []string, reason string, nowMs int64, denyUntilMs int64) error {
	if denyUntilMs <= nowMs {
		return sql.ErrNoRows
	}
	for _, sessionID := range sessionIDs {
		// unique key 让 repeated replay/logout 复用同一事实。若新撤销把 deny_until 延长，必须重新 pending；
		// 否则 DB commit 后进程崩溃会让旧 delivered 状态掩盖尚未写入 Redis 的新 TTL。
		if _, err := tx.ExecContext(ctx, `
			INSERT INTO auth_session_denylist_jobs
			  (app_code, session_id, reason, status, attempts, next_retry_at_ms, locked_by, locked_until_ms, last_error, deny_until_ms, created_at_ms, updated_at_ms)
			VALUES (?, ?, ?, 'pending', 0, ?, '', 0, '', ?, ?, ?)
			ON DUPLICATE KEY UPDATE
			  next_retry_at_ms = IF(VALUES(deny_until_ms) > deny_until_ms OR status <> 'delivered', VALUES(next_retry_at_ms), next_retry_at_ms),
			  locked_by = IF(VALUES(deny_until_ms) > deny_until_ms OR status <> 'delivered', '', locked_by),
			  locked_until_ms = IF(VALUES(deny_until_ms) > deny_until_ms OR status <> 'delivered', 0, locked_until_ms),
			  status = IF(VALUES(deny_until_ms) > deny_until_ms OR status <> 'delivered', 'pending', 'delivered'),
			  reason = IF(VALUES(deny_until_ms) >= deny_until_ms, VALUES(reason), reason),
			  deny_until_ms = GREATEST(deny_until_ms, VALUES(deny_until_ms)),
			  updated_at_ms = VALUES(updated_at_ms)
		`, appcode.Normalize(appCode), sessionID, reason, nowMs, denyUntilMs, nowMs, nowMs); err != nil {
			return err
		}
	}
	return nil
}

func (r *Repository) ClaimSessionDenylistJobs(ctx context.Context, workerID string, nowMs int64, lockUntilMs int64, limit int) ([]authdomain.SessionDenylistJob, error) {
	if limit <= 0 {
		limit = 100
	}
	tx, err := r.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	rows, err := tx.QueryContext(ctx, `
		SELECT job_id, app_code, session_id, reason, attempts, deny_until_ms, created_at_ms
		FROM auth_session_denylist_jobs
		WHERE deny_until_ms > ? AND ((status IN ('pending', 'retryable') AND next_retry_at_ms <= ?)
		   OR (status = 'processing' AND locked_until_ms <= ?)
		)
		ORDER BY next_retry_at_ms, job_id
		LIMIT ?
		FOR UPDATE SKIP LOCKED
	`, nowMs, nowMs, nowMs, limit)
	if err != nil {
		return nil, err
	}
	var jobs []authdomain.SessionDenylistJob
	for rows.Next() {
		var job authdomain.SessionDenylistJob
		if err := rows.Scan(&job.JobID, &job.AppCode, &job.SessionID, &job.Reason, &job.Attempts, &job.DenyUntilMs, &job.CreatedAtMs); err != nil {
			_ = rows.Close()
			return nil, err
		}
		jobs = append(jobs, job)
	}
	if err := rows.Close(); err != nil {
		return nil, err
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	for _, job := range jobs {
		if _, err := tx.ExecContext(ctx, `
			UPDATE auth_session_denylist_jobs
			SET status = 'processing', attempts = attempts + 1, locked_by = ?, locked_until_ms = ?, updated_at_ms = ?
			WHERE job_id = ?
		`, strings.TrimSpace(workerID), lockUntilMs, nowMs, job.JobID); err != nil {
			return nil, err
		}
	}
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	return jobs, nil
}

func (r *Repository) MarkSessionDenylistJobDelivered(ctx context.Context, jobID int64, appCode string, sessionID string, reason string, appliedDenyUntilMs int64, nowMs int64, nextRehydrateAtMs int64) error {
	if jobID > 0 {
		_, err := r.db.ExecContext(ctx, `
			UPDATE auth_session_denylist_jobs
			SET status = 'delivered', next_retry_at_ms = ?, locked_by = '', locked_until_ms = 0, last_error = '', updated_at_ms = ?
			WHERE job_id = ? AND deny_until_ms <= ?
		`, nextRehydrateAtMs, nowMs, jobID, appliedDenyUntilMs)
		return err
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE auth_session_denylist_jobs
		SET status = 'delivered', next_retry_at_ms = ?, locked_by = '', locked_until_ms = 0, last_error = '', updated_at_ms = ?
		WHERE app_code = ? AND session_id = ? AND deny_until_ms <= ?
	`, nextRehydrateAtMs, nowMs, appcode.Normalize(appCode), sessionID, appliedDenyUntilMs)
	return err
}

func (r *Repository) MarkSessionDenylistJobRetryable(ctx context.Context, jobID int64, errorMessage string, nextRetryAtMs int64, nowMs int64) error {
	if len(errorMessage) > 512 {
		errorMessage = errorMessage[:512]
	}
	_, err := r.db.ExecContext(ctx, `
		UPDATE auth_session_denylist_jobs
		SET status = 'retryable', next_retry_at_ms = ?, locked_by = '', locked_until_ms = 0, last_error = ?, updated_at_ms = ?
		WHERE job_id = ?
	`, nextRetryAtMs, errorMessage, nowMs, jobID)
	return err
}

func (r *Repository) MaxSessionDenylistJobID(ctx context.Context) (int64, error) {
	var maxJobID int64
	err := r.db.QueryRowContext(ctx, `SELECT COALESCE(MAX(job_id), 0) FROM auth_session_denylist_jobs`).Scan(&maxJobID)
	return maxJobID, err
}

func (r *Repository) ListSessionDenylistJobsForHydration(ctx context.Context, nowMs int64, afterJobID int64, throughJobID int64, limit int) ([]authdomain.SessionDenylistJob, int64, error) {
	if limit <= 0 {
		limit = 1000
	}
	// hydration 是只读 keyset snapshot：不看 status/lease、不 SKIP LOCKED，也不依赖旧 pod 的 worker 状态。
	rows, err := r.db.QueryContext(ctx, `
		SELECT job_id, app_code, session_id, reason, attempts, deny_until_ms, created_at_ms
		FROM auth_session_denylist_jobs
		WHERE deny_until_ms > ? AND job_id > ? AND job_id <= ?
		ORDER BY job_id
		LIMIT ?
	`, nowMs, afterJobID, throughJobID, limit)
	if err != nil {
		return nil, afterJobID, err
	}
	defer rows.Close()
	jobs := make([]authdomain.SessionDenylistJob, 0, limit)
	for rows.Next() {
		var job authdomain.SessionDenylistJob
		if err := rows.Scan(&job.JobID, &job.AppCode, &job.SessionID, &job.Reason, &job.Attempts, &job.DenyUntilMs, &job.CreatedAtMs); err != nil {
			return nil, afterJobID, err
		}
		jobs = append(jobs, job)
	}
	if err := rows.Err(); err != nil {
		return nil, afterJobID, err
	}
	if len(jobs) == 0 {
		return jobs, afterJobID, nil
	}
	return jobs, jobs[len(jobs)-1].JobID, nil
}

func (r *Repository) DeleteExpiredSessionDenylistJobs(ctx context.Context, nowMs int64, limit int) (int, error) {
	if limit <= 0 {
		limit = 1000
	}
	result, err := r.db.ExecContext(ctx, `
		DELETE FROM auth_session_denylist_jobs
		WHERE deny_until_ms <= ?
		ORDER BY deny_until_ms, job_id
		LIMIT ?
	`, nowMs, limit)
	if err != nil {
		return 0, err
	}
	affected, err := result.RowsAffected()
	return int(affected), err
}

func (r *Repository) DeleteExpiredRefreshOutcomes(ctx context.Context, nowMs int64, limit int) (int, error) {
	if limit <= 0 {
		limit = 1000
	}
	result, err := r.db.ExecContext(ctx, `
		DELETE FROM auth_refresh_outcomes
		WHERE expires_at_ms < ?
		ORDER BY expires_at_ms
		LIMIT ?
	`, nowMs, limit)
	if err != nil {
		return 0, err
	}
	affected, err := result.RowsAffected()
	return int(affected), err
}
